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,247 @@
1
+ # A nested class with no body declares that something exists, most often an
2
+ # error. Written as a constant it says so in one line and stops looking like a
3
+ # class that lost its body.
4
+ #
5
+ # Only nested definitions are reported. A class living in its own file is that
6
+ # file's subject, and `class` reads as the heading it is.
7
+ #
8
+ # @example
9
+ # # bad
10
+ # class Configuration
11
+ # class Error < StandardError; end
12
+ #
13
+ # def call
14
+ # end
15
+ # end
16
+ #
17
+ # # good
18
+ # class Configuration
19
+ # Error = Class.new(StandardError)
20
+ #
21
+ # def call
22
+ # end
23
+ # end
24
+ #
25
+ class RuboCop::Cop::Callbacksystems::PreferClassNewForEmptyNestedClass < RuboCop::Cop::Callbacksystems::Base
26
+ include RuboCop::Callbacksystems::ProjectIndex::Support
27
+ extend RuboCop::Cop::AutoCorrector
28
+
29
+ def on_class(node)
30
+ report EmptyNestedClass.new \
31
+ node,
32
+ RuboCop::Callbacksystems::Source::Comments.for(processed_source),
33
+ assignment_safe: assignment_safe?(node)
34
+ end
35
+
36
+ private
37
+ def assignment_safe?(node)
38
+ if node.identifier.namespace.nil? && project_index_reliable?
39
+ declaration = resolve_constant_in_index(node.identifier)
40
+
41
+ declaration.is_a?(Rubydex::Class) && DeclarationHistory.new(
42
+ declaration,
43
+ node,
44
+ file: processed_source.file_path
45
+ ).new_at_node?
46
+ else
47
+ false
48
+ end
49
+ end
50
+
51
+ # A class declaration can become an assignment only when this node creates the constant. Later declarations in the
52
+ # same file still see that class, while an earlier or externally ordered definition could be overwritten.
53
+ class DeclarationHistory
54
+ def initialize(declaration, node, file:)
55
+ @declaration = declaration
56
+ @node = node
57
+ @file = file
58
+ end
59
+
60
+ def new_at_node?
61
+ current_definition && owners_known? && definitions.all? do |definition|
62
+ definition.equal?(current_definition) || later_in_current_file?(definition)
63
+ end
64
+ end
65
+
66
+ private
67
+ attr_reader :declaration, :node, :file
68
+
69
+ def current_definition
70
+ @current_definition ||= definitions.find do |definition|
71
+ definition.is_a?(Rubydex::ClassDefinition) && same_location?(definition.location, node.source_range)
72
+ end
73
+ end
74
+
75
+ def definitions
76
+ @definitions ||= declaration.definitions.to_a
77
+ end
78
+
79
+ def same_location?(location, range)
80
+ same_file?(location) && RuboCop::Callbacksystems::ProjectIndex::Location.for_rubydex(location) ==
81
+ RuboCop::Callbacksystems::ProjectIndex::Location.for_ast(range, uri: location.uri)
82
+ end
83
+
84
+ def same_file?(location)
85
+ File.identical?(location.to_file_path, file)
86
+ rescue SystemCallError, Rubydex::Location::NotFileUriError
87
+ false
88
+ end
89
+
90
+ # A definition beneath a placeholder namespace can be real, but Rubydex cannot prove what an autoloader may
91
+ # already have installed there. Every owner up to Object therefore needs a declaration of its own.
92
+ def owners_known?
93
+ owner_chain_known_from?(declaration.owner)
94
+ end
95
+
96
+ def owner_chain_known_from?(owner)
97
+ owner.name == "Object" || (owner.definitions.any? && owner_chain_known_from?(owner.owner))
98
+ end
99
+
100
+ def later_in_current_file?(definition)
101
+ same_file?(definition.location) && after_current_definition?(definition.location)
102
+ end
103
+
104
+ def after_current_definition?(location)
105
+ ([ location.start_line, location.start_column ] <=> current_position).positive?
106
+ end
107
+
108
+ def current_position
109
+ @current_position ||= current_definition.location.then { [ it.start_line, it.start_column ] }
110
+ end
111
+ end
112
+
113
+ class EmptyNestedClass
114
+ include RuboCop::Callbacksystems::Helpers
115
+
116
+ MESSAGE = "Declare `%<name>s` as `Class.new` instead of a class definition with no body."
117
+
118
+ def initialize(node, source_comments, assignment_safe:)
119
+ @node = node
120
+ @source_comments = source_comments
121
+ @assignment_safe = assignment_safe
122
+ end
123
+
124
+ def offense
125
+ if node.body.nil? && nested?
126
+ RuboCop::Callbacksystems::Offense.new(node, message, correcting: correctable?) { correct(it) }
127
+ end
128
+ end
129
+
130
+ private
131
+ attr_reader :node, :source_comments, :assignment_safe
132
+
133
+ def nested?
134
+ enclosing_class_or_module_of(node).present?
135
+ end
136
+
137
+ def message
138
+ format(MESSAGE, name: class_name_of(node))
139
+ end
140
+
141
+ def correctable?
142
+ assignment_safe && expression_result.discarded? && superclass_without_heredoc? &&
143
+ comments.none? { tooling_comment?(it) }
144
+ end
145
+
146
+ def expression_result
147
+ @expression_result ||= RuboCop::Callbacksystems::Execution::DiscardedExpression.new(node)
148
+ end
149
+
150
+ def superclass_without_heredoc?
151
+ node.parent_class.nil? || !carries_heredoc?(node.parent_class)
152
+ end
153
+
154
+ def comments
155
+ @comments ||= [ *source_comments.within(node), closing_comment ]
156
+ .compact.uniq.sort_by { it.source_range.begin_pos }
157
+ end
158
+
159
+ def closing_comment
160
+ @closing_comment ||= source_comments.trailing_comment_on(node.last_line)&.then do |comment|
161
+ comment if directly_trails_definition?(comment)
162
+ end
163
+ end
164
+
165
+ def directly_trails_definition?(comment)
166
+ node.source_range.with(begin_pos: node.source_range.end_pos, end_pos: comment.source_range.begin_pos)
167
+ .source.match?(/\A[ \t]*;?[ \t]*\z/)
168
+ end
169
+
170
+ def correct(corrector)
171
+ corrector.replace(correction_range, corrected_source)
172
+ end
173
+
174
+ def correction_range
175
+ closing_comment ? node.source_range.join(closing_comment.source_range) : node.source_range
176
+ end
177
+
178
+ def corrected_source
179
+ CommentedDeclaration.new(declaration, movable_comments, indentation:).source
180
+ end
181
+
182
+ def declaration
183
+ "#{class_name_of(node)} = Class.new#{inherited_class}"
184
+ end
185
+
186
+ def inherited_class
187
+ "(#{node.parent_class.source})" if node.parent_class
188
+ end
189
+
190
+ def movable_comments
191
+ comments.reject { superclass_contains?(it) }
192
+ end
193
+
194
+ def superclass_contains?(comment)
195
+ node.parent_class&.source_range&.contains?(comment.source_range)
196
+ end
197
+
198
+ def indentation
199
+ preceding = node.source_range.source_line[0...node.source_range.column]
200
+
201
+ preceding.strip.empty? ? preceding : " " * node.source_range.column
202
+ end
203
+
204
+ # Comments from the definition keep their order around the declaration, with one former trailing comment still
205
+ # trailing code and every other comment standing on its own line.
206
+ class CommentedDeclaration
207
+ include RuboCop::Callbacksystems::Helpers
208
+
209
+ def initialize(declaration, comments, indentation:)
210
+ @declaration = declaration
211
+ @comments = comments
212
+ @indentation = indentation
213
+ end
214
+
215
+ def source
216
+ [ *before, declaration_with_comment, *after ].join("\n#{indentation}")
217
+ end
218
+
219
+ private
220
+ attr_reader :declaration, :comments, :indentation
221
+
222
+ def before
223
+ comments.take(anchor_position).map(&:text)
224
+ end
225
+
226
+ def anchor_position
227
+ @anchor_position ||= comments.index { !own_line_comment?(it) } || comments.size
228
+ end
229
+
230
+ def declaration_with_comment
231
+ [ declaration, anchor&.text ].compact.join(" ")
232
+ end
233
+
234
+ def anchor
235
+ comments[anchor_position] unless anchor_position == comments.size
236
+ end
237
+
238
+ def after
239
+ comments.drop(anchor_position + anchor_count).map(&:text)
240
+ end
241
+
242
+ def anchor_count
243
+ anchor ? 1 : 0
244
+ end
245
+ end
246
+ end
247
+ end
@@ -0,0 +1,124 @@
1
+ # A `Data.define`, `Struct.new` or `Class.new` given a block defines a class
2
+ # with behavior, so it reads better as the class it is.
3
+ #
4
+ # The block form also changes what the code inside it means. A constant
5
+ # declared there lands in the enclosing namespace instead of the new class,
6
+ # `Module.nesting` never mentions the class, and the rules that govern a class
7
+ # body do not reach inside a block.
8
+ #
9
+ # A builder with no block only lists its members and stays as it is.
10
+ #
11
+ # @example
12
+ # # bad
13
+ # Route = Data.define(:host) do
14
+ # def matches?(other)
15
+ # host == other
16
+ # end
17
+ # end
18
+ #
19
+ # # good
20
+ # class Route < Data.define(:host)
21
+ # def matches?(other)
22
+ # host == other
23
+ # end
24
+ # end
25
+ #
26
+ # # good - nothing but its members
27
+ # Entry = Data.define(:sku, :quantity)
28
+ #
29
+ class RuboCop::Cop::Callbacksystems::PreferClassOverBuilderBlock < RuboCop::Cop::Callbacksystems::Base
30
+ extend RuboCop::Cop::AutoCorrector
31
+
32
+ def on_casgn(node)
33
+ report BuilderBlock.new(node)
34
+ end
35
+
36
+ private
37
+ class BuilderBlock
38
+ include RuboCop::Callbacksystems::Helpers
39
+
40
+ MESSAGE = "Define `%<name>s` as a class instead of a block given to `%<builder>s`."
41
+
42
+ def initialize(node)
43
+ @node = node
44
+ end
45
+
46
+ def offense
47
+ if class_with_body?(node)
48
+ RuboCop::Callbacksystems::Offense.new(node, message, correcting: rewritable?) { correct(it) }
49
+ end
50
+ end
51
+
52
+ private
53
+ attr_reader :node
54
+
55
+ def message
56
+ format(MESSAGE, name: assigned_name, builder: builder_name)
57
+ end
58
+
59
+ def assigned_name
60
+ node.source_range.with(end_pos: node.loc.operator.begin_pos).source.rstrip
61
+ end
62
+
63
+ def builder_name
64
+ "#{builder_call.receiver.source}.#{builder_call.method_name}"
65
+ end
66
+
67
+ def builder_call
68
+ block.send_node
69
+ end
70
+
71
+ def block
72
+ node.expression
73
+ end
74
+
75
+ # Only `do ... end` is rewritten, since anything else needs the body moved onto its own lines to stay valid.
76
+ def rewritable?
77
+ block.multiline? && block.loc.begin.source == "do" && block.argument_list.empty? &&
78
+ !captures_local_variable? && !reads_or_assigns_constant? && !reads_or_assigns_class_variable? &&
79
+ !builder_control_flow?
80
+ end
81
+
82
+ def captures_local_variable?
83
+ nodes_in(block.body, :lvar).any? do |read|
84
+ read.each_ancestor(:any_def).none? &&
85
+ !name_rebound_between?(read.name, node: read, boundary: block)
86
+ end
87
+ end
88
+
89
+ def reads_or_assigns_constant?
90
+ nodes_in(block.body, :const, :casgn).any?
91
+ end
92
+
93
+ def reads_or_assigns_class_variable?
94
+ nodes_in(block.body, :cvar, :cvasgn).any?
95
+ end
96
+
97
+ def builder_control_flow?
98
+ nodes_in(block.body, :break, :next, :redo).any? do |flow|
99
+ flow.each_ancestor(*BLOCK_NODE_TYPES).first.equal?(block)
100
+ end
101
+ end
102
+
103
+ def correct(corrector)
104
+ corrector.replace(header, "class #{assigned_name}#{inheritance}")
105
+ end
106
+
107
+ def header
108
+ node.source_range.with(end_pos: block.loc.begin.end_pos)
109
+ end
110
+
111
+ # `Class.new(Base)` is a plain subclass of `Base`, while `Data.define` and `Struct.new` build the superclass.
112
+ def inheritance
113
+ plain_subclass? ? argument_inheritance : " < #{builder_call.source}"
114
+ end
115
+
116
+ def plain_subclass?
117
+ builder_call.receiver.short_name == :Class
118
+ end
119
+
120
+ def argument_inheritance
121
+ " < #{builder_call.first_argument.source}" if builder_call.first_argument
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,225 @@
1
+ # Detects a parameter given its fallback value at the top of the method. A
2
+ # `||=` or a `nil?` guard opening the body makes the reader work out what the
3
+ # parameter really is, where a default in the signature says it right where the
4
+ # parameter is declared, so the body starts with the work.
5
+ #
6
+ # The cop recommends the change for a positional parameter and a keyword
7
+ # declared as `name: nil`. It leaves a required keyword alone, since a default
8
+ # would change what callers must pass, as well as a fallback reading a parameter
9
+ # declared to its right, which a default cannot see. Only the run of fallbacks
10
+ # opening the body counts: one that follows other statements may depend on them.
11
+ #
12
+ # The offense has no automatic correction. A default only runs when an argument
13
+ # is omitted, while these fallbacks also run for an explicit `nil` or `false`;
14
+ # making a required positional parameter optional changes its observable arity
15
+ # too. Choosing the new calling contract belongs to the author.
16
+ #
17
+ # @example
18
+ # # bad
19
+ # def deliver(recipients)
20
+ # recipients ||= [ owner ]
21
+ # recipients.each { mail(it) }
22
+ # end
23
+ #
24
+ # # bad
25
+ # def deliver(recipients: nil)
26
+ # recipients = [ owner ] if recipients.nil?
27
+ # recipients.each { mail(it) }
28
+ # end
29
+ #
30
+ # # good
31
+ # def deliver(recipients = [ owner ])
32
+ # recipients.each { mail(it) }
33
+ # end
34
+ #
35
+ # # good
36
+ # def deliver(recipients: [ owner ])
37
+ # recipients.each { mail(it) }
38
+ # end
39
+ #
40
+ # # good - a default cannot read a parameter to its right
41
+ # def deliver(recipients, sender)
42
+ # recipients ||= [ sender ]
43
+ # recipients.each { mail(it) }
44
+ # end
45
+ #
46
+ class RuboCop::Cop::Callbacksystems::PreferDefaultArgument < RuboCop::Cop::Callbacksystems::Base
47
+ def on_def(node)
48
+ report_each LeadingFallbacks.new(node)
49
+ end
50
+
51
+ alias on_defs on_def
52
+
53
+ private
54
+ # The fallbacks opening a method body, one per parameter, narrowed to those expressible as a signature default.
55
+ class LeadingFallbacks
56
+ include RuboCop::Callbacksystems::Helpers
57
+
58
+ def initialize(method_node)
59
+ @method_node = method_node
60
+ end
61
+
62
+ def each_offense
63
+ reportable.each { yield it.offense } if followed_by_work?
64
+ end
65
+
66
+ private
67
+ attr_reader :method_node
68
+
69
+ def followed_by_work?
70
+ statements.size > leading.size
71
+ end
72
+
73
+ def statements
74
+ statements_in(method_node.body)
75
+ end
76
+
77
+ def leading
78
+ @leading ||= statements.map { Fallback.new(it, method_node) }.take_while(&:fallback?).uniq(&:name)
79
+ end
80
+
81
+ # Ruby allows one group of optional positional parameters, so a recommendation must not open a second one.
82
+ def reportable
83
+ optional_group_stays_contiguous? ? candidates : candidates.select(&:keyword?)
84
+ end
85
+
86
+ def optional_group_stays_contiguous?
87
+ optional_runs.size <= 1
88
+ end
89
+
90
+ def optional_runs
91
+ positional_parameters
92
+ .chunk_while { |left, right| optional_with_candidate?(left) == optional_with_candidate?(right) }
93
+ .select { optional_with_candidate?(it.first) }
94
+ end
95
+
96
+ def positional_parameters
97
+ method_node.arguments.select { it.type?(:arg, :optarg) }
98
+ end
99
+
100
+ def optional_with_candidate?(parameter)
101
+ parameter.optarg_type? || candidates.any? { it.parameter.equal?(parameter) }
102
+ end
103
+
104
+ def candidates
105
+ @candidates ||= leading.select(&:reportable?)
106
+ end
107
+ end
108
+
109
+ class Fallback
110
+ include RuboCop::Callbacksystems::Helpers
111
+ extend RuboCop::AST::NodePattern::Macros
112
+
113
+ MESSAGE = "Give `%<name>s` its default in the signature instead of a fallback in the body."
114
+
115
+ # @!method fallback_of(node)
116
+ def_node_matcher :fallback_of, <<~PATTERN
117
+ {
118
+ (or_asgn (lvasgn $_name) $_value)
119
+ (if (send (lvar $_name) :nil?) (lvasgn _name $_value) nil?)
120
+ (if (lvar $_name) nil? (lvasgn _name $_value))
121
+ }
122
+ PATTERN
123
+
124
+ def initialize(statement, method_node)
125
+ @statement = statement
126
+ @method_node = method_node
127
+ end
128
+
129
+ def fallback?
130
+ parameter.present?
131
+ end
132
+
133
+ def parameter
134
+ @parameter ||= named_parameters.find { it.name == name } if name
135
+ end
136
+
137
+ def name
138
+ captures&.first
139
+ end
140
+
141
+ def keyword?
142
+ parameter.kwoptarg_type?
143
+ end
144
+
145
+ def reportable?
146
+ defaultable? && value.single_line? && !carries_heredoc?(value) && !reads_parameter_from_itself_on? &&
147
+ alone_on_its_lines?
148
+ end
149
+
150
+ def offense
151
+ RuboCop::Callbacksystems::Offense.new(statement, format(MESSAGE, name:))
152
+ end
153
+
154
+ private
155
+ attr_reader :statement, :method_node
156
+
157
+ def captures
158
+ @captures ||= fallback_of(statement)
159
+ end
160
+
161
+ def named_parameters
162
+ method_node.arguments.reject(&:mlhs_type?)
163
+ end
164
+
165
+ def defaultable?
166
+ (parameter.arg_type? && !positional_parameter_after_rest?) ||
167
+ (keyword? && parameter.default_value.nil_type?)
168
+ end
169
+
170
+ def positional_parameter_after_rest?
171
+ method_node.arguments.take_while { !it.equal?(parameter) }.any?(&:restarg_type?)
172
+ end
173
+
174
+ def value
175
+ captures.second
176
+ end
177
+
178
+ def reads_parameter_from_itself_on?
179
+ nodes_in(value, :lvar).any? do |read|
180
+ parameter_names_from_itself_on.include?(read.name) &&
181
+ !name_rebound_between?(read.name, node: read, boundary: method_node)
182
+ end
183
+ end
184
+
185
+ def parameter_names_from_itself_on
186
+ method_node.arguments.drop_while { !it.equal?(parameter) }.flat_map { ParameterNames.new(it).to_a }
187
+ end
188
+
189
+ def alone_on_its_lines?
190
+ statement.first_line > method_node.first_line && statement.last_line < next_code_line
191
+ end
192
+
193
+ def next_code_line
194
+ statement.right_sibling.first_line
195
+ end
196
+ end
197
+
198
+ # Names introduced by one argument, including the leaves of a destructured positional argument.
199
+ class ParameterNames
200
+ include Enumerable
201
+
202
+ def initialize(parameter)
203
+ @parameter = parameter
204
+ end
205
+
206
+ def each
207
+ if block_given?
208
+ pending = [ parameter ]
209
+ until pending.empty?
210
+ current = pending.pop
211
+ if current.mlhs_type?
212
+ pending.concat(current.each_child_node.to_a.reverse)
213
+ elsif current.respond_to?(:name) && current.name
214
+ yield current.name
215
+ end
216
+ end
217
+ else
218
+ to_enum(__method__)
219
+ end
220
+ end
221
+
222
+ private
223
+ attr_reader :parameter
224
+ end
225
+ end