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,57 @@
1
+ # Constants are read, never mutated, so freezing one guards against a mistake
2
+ # nobody makes and puts noise on every declaration. Core `Style/MutableConstant`
3
+ # asks for the opposite and stays off, while `Style/RedundantFreeze` reports the
4
+ # same line whenever the value was immutable already.
5
+ #
6
+ # @example
7
+ # # bad
8
+ # FORMATS = [ :json ].freeze
9
+ # TIMEOUT = "30s".freeze
10
+ #
11
+ # # good
12
+ # FORMATS = [ :json ]
13
+ # TIMEOUT = "30s"
14
+ #
15
+ class RuboCop::Cop::Callbacksystems::RedundantConstantFreeze < RuboCop::Cop::Callbacksystems::Base
16
+ extend RuboCop::Cop::AutoCorrector
17
+
18
+ MESSAGE = "Remove `freeze`. A constant is read, not mutated."
19
+
20
+ def on_casgn(node)
21
+ call = frozen_call_in(node)
22
+
23
+ report call if call
24
+ end
25
+
26
+ private
27
+ def frozen_call_in(node)
28
+ expression = node.children.last
29
+ expression = expression.children.first while parenthesized_expression?(expression)
30
+ expression if freezing_call?(expression)
31
+ end
32
+
33
+ def parenthesized_expression?(node)
34
+ node.begin_type? && node.children.one?
35
+ end
36
+
37
+ def freezing_call?(node)
38
+ node.call_type? && node.receiver && freezes_without_arguments?(node)
39
+ end
40
+
41
+ def freezes_without_arguments?(node)
42
+ node.method?(:freeze) && node.arguments.empty?
43
+ end
44
+
45
+ def report(call)
46
+ range = freeze_range_of(call)
47
+ if source_comments.any_within?(range)
48
+ add_offense(range, message: MESSAGE)
49
+ else
50
+ add_offense(range, message: MESSAGE) { it.remove(range) }
51
+ end
52
+ end
53
+
54
+ def freeze_range_of(call)
55
+ call.source_range.with(begin_pos: call.loc.dot.begin_pos)
56
+ end
57
+ end
@@ -0,0 +1,134 @@
1
+ # A constant reference does not need to repeat the namespace it is already
2
+ # written inside. The prefix is noise on every read, and it has to be rewritten
3
+ # whenever the namespace moves, while the bare name resolves through the scope
4
+ # the file already opens.
5
+ #
6
+ # Only the innermost lexical scope counts. Under the compact class style a
7
+ # definition like `class PgBox::Configuration` opens a single scope, so `PgBox`
8
+ # itself is never searched and its prefix has to stay.
9
+ #
10
+ # @example
11
+ # # bad
12
+ # class PgBox::Configuration
13
+ # def validator
14
+ # PgBox::Configuration::Validator
15
+ # end
16
+ # end
17
+ #
18
+ # # good
19
+ # class PgBox::Configuration
20
+ # def validator
21
+ # Validator
22
+ # end
23
+ # end
24
+ #
25
+ # # good - `PgBox` is not a lexical scope here, so the prefix is required
26
+ # class PgBox::Configuration
27
+ # def secrets
28
+ # PgBox::Secrets
29
+ # end
30
+ # end
31
+ #
32
+ class RuboCop::Cop::Callbacksystems::RedundantConstantNamespace < RuboCop::Cop::Callbacksystems::Base
33
+ include RuboCop::Callbacksystems::ProjectIndex::Support
34
+ extend RuboCop::Cop::AutoCorrector
35
+
36
+ def on_new_investigation
37
+ @constant_references = if project_index_reliable?
38
+ RuboCop::Callbacksystems::ProjectIndex::ConstantReferences.for(project_index).within(processed_source)
39
+ end
40
+ end
41
+
42
+ def on_const(node)
43
+ report ConstantReference.new(node, constant_references) if constant_references
44
+ end
45
+
46
+ private
47
+ attr_reader :constant_references
48
+
49
+ class ConstantReference
50
+ include RuboCop::Callbacksystems::Helpers
51
+
52
+ MESSAGE = "Remove the redundant `%<namespace>s::` prefix; the constant already resolves from here."
53
+
54
+ def initialize(node, constant_references)
55
+ @node = node
56
+ @constant_references = constant_references
57
+ end
58
+
59
+ def offense
60
+ RuboCop::Callbacksystems::Offense.new(node, message) { correct(it) } if reference? && redundantly_qualified?
61
+ end
62
+
63
+ private
64
+ attr_reader :node, :constant_references
65
+
66
+ def reference?
67
+ !node.parent&.const_type? && !definition_identifier?(node)
68
+ end
69
+
70
+ def redundantly_qualified?
71
+ enclosing_scope_name.present? && qualified_node.present? && explicitly_declared_scope? && same_declaration?
72
+ end
73
+
74
+ def enclosing_scope_name
75
+ @enclosing_scope_name ||= enclosing_definitions.map { constant_name_of(it.identifier) }.join("::")
76
+ end
77
+
78
+ def enclosing_definitions
79
+ node.each_ancestor(:class, :module).to_a.reverse
80
+ end
81
+
82
+ def qualified_node
83
+ @qualified_node ||= constant_chain.find { constant_name_of(it.namespace) == enclosing_scope_name }
84
+ end
85
+
86
+ def constant_chain
87
+ [ node, *node.each_descendant(:const) ].select(&:namespace)
88
+ end
89
+
90
+ def explicitly_declared_scope?
91
+ enclosing_scope_chain.present? && enclosing_scope_chain.all? { declared_namespace?(it) } &&
92
+ enclosing_scope_chain.last.owner&.name == "Object"
93
+ end
94
+
95
+ def enclosing_scope_chain
96
+ @enclosing_scope_chain ||= Enumerator.produce(enclosing_scope_declaration) { it&.owner }
97
+ .take_while { it.present? && it.name != "Object" }
98
+ end
99
+
100
+ def enclosing_scope_declaration
101
+ constant_references.referenced_declaration_for(qualified_node.namespace)
102
+ end
103
+
104
+ def declared_namespace?(declaration)
105
+ declaration.is_a?(Rubydex::Namespace) && declared?(declaration)
106
+ end
107
+
108
+ def declared?(declaration)
109
+ declaration&.definitions&.any?
110
+ end
111
+
112
+ def same_declaration?
113
+ written_declaration = constant_references.referenced_declaration_for(node)
114
+ shortened_declaration = constant_references.declaration_named(shortened_name, beside: node)
115
+
116
+ declared?(written_declaration) && declared?(shortened_declaration) &&
117
+ written_declaration.name == shortened_declaration.name
118
+ end
119
+
120
+ def shortened_name
121
+ chain = [ node ]
122
+ chain << chain.last.namespace until chain.last.equal?(qualified_node)
123
+ chain.reverse_each.map(&:short_name).join("::")
124
+ end
125
+
126
+ def message
127
+ format(MESSAGE, namespace: enclosing_scope_name)
128
+ end
129
+
130
+ def correct(corrector)
131
+ corrector.remove(node.source_range.with(end_pos: qualified_node.loc.name.begin_pos))
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,133 @@
1
+ # A comment block wrapped to a narrower column than the project uses costs the
2
+ # reader extra lines for nothing. When a line breaks mid-sentence and the first
3
+ # word of the next line still fits under the Layout/LineLength Max, the block
4
+ # is refilled to the full width. A break on a sentence boundary may be
5
+ # deliberate, and a block carrying a list, an indented example, a code tail,
6
+ # a backtick fence, or an unbreakable word keeps the shape its author gave it.
7
+ # A line opening with a label (`Slot 1: ...`) or reading as code is an item of
8
+ # its own, so it keeps its line too.
9
+ #
10
+ # @example
11
+ # # bad - the break falls mid-sentence with room left on the line
12
+ # # A comment that stops
13
+ # # mid-sentence.
14
+ #
15
+ # # good - filled to the full width
16
+ # # A comment that stops mid-sentence.
17
+ #
18
+ # # good - every break lands on a sentence boundary
19
+ # # First idea ends here.
20
+ # # Second idea starts here.
21
+ #
22
+ class RuboCop::Cop::Callbacksystems::ReflowComments < RuboCop::Cop::Callbacksystems::Base
23
+ extend RuboCop::Cop::AutoCorrector
24
+
25
+ def on_new_investigation
26
+ comment_blocks_in(processed_source, reject_tooling_runs: true).each do |comments|
27
+ report ProseBlock.new(comments, max_line_length), rewrites_comments: true
28
+ end
29
+ end
30
+
31
+ private
32
+ class ProseBlock
33
+ include RuboCop::Callbacksystems::Helpers
34
+
35
+ MESSAGE = "This comment breaks mid-sentence with room left on the line. Fill it to %d columns."
36
+ MARKER_WIDTH = "# ".length
37
+ SENTENCE_END = /[.:;!?]["'`)\]]?\z/
38
+ STRUCTURE = /\A(?:\s|[-*|>+@#=]|\d+[.)]|:[a-z][\w-]*:|(?:--|\+\+)\z)/i
39
+ CODE_TAIL = /[{(=]\z/
40
+ LABELED = /\A[A-Za-z][^:\n]{0,30}:\s/
41
+ CODE = /\A[\w.]+(?:\(|\s+[A-Z]\w*::|\s+:\w|\s+["']|\s*=\s)/
42
+
43
+ def initialize(comments, max_length)
44
+ @comments = comments
45
+ @max_length = max_length
46
+ end
47
+
48
+ def offense
49
+ RuboCop::Callbacksystems::Offense.new(range, message) { correct(it) } if narrow_wrap?
50
+ end
51
+
52
+ private
53
+ attr_reader :comments, :max_length
54
+
55
+ def narrow_wrap?
56
+ prose? && lines.each_index.any? { absorbs_next?(it) }
57
+ end
58
+
59
+ def prose?
60
+ lines.all? { prose_line?(it) }
61
+ end
62
+
63
+ def lines
64
+ @lines ||= comments.map { it.text.delete_prefix("#").delete_prefix(" ").rstrip }
65
+ end
66
+
67
+ def prose_line?(line)
68
+ !line.match?(STRUCTURE) && !line.match?(CODE_TAIL) && line.exclude?("```") &&
69
+ line.split.all? { width_of(it) <= max_length }
70
+ end
71
+
72
+ def width_of(text)
73
+ indent + MARKER_WIDTH + text.length
74
+ end
75
+
76
+ def indent
77
+ comments.first.source_range.column
78
+ end
79
+
80
+ def absorbs_next?(index)
81
+ continues_at?(index) && width_of("#{lines[index]} #{lines[index + 1].split.first}") <= max_length
82
+ end
83
+
84
+ # A line opening with a label or reading as code is an item of its own, not the tail of the sentence above it.
85
+ def continues_at?(index)
86
+ following = lines[index + 1]
87
+ following.present? && !lines[index].empty? && !lines[index].match?(SENTENCE_END) &&
88
+ !following.match?(LABELED) && !following.match?(CODE)
89
+ end
90
+
91
+ def range
92
+ range_spanning(comments)
93
+ end
94
+
95
+ def message
96
+ format(MESSAGE, max_length)
97
+ end
98
+
99
+ def correct(corrector)
100
+ corrector.replace(range, filled_text)
101
+ end
102
+
103
+ def filled_text
104
+ paragraphs.flat_map { filled(it) }.join("\n#{" " * indent}")
105
+ end
106
+
107
+ def paragraphs
108
+ lines.each_index.slice_when { |index, _| !continues_at?(index) }
109
+ .map { lines.values_at(*it) }
110
+ .flat_map { detached_blank(it) }
111
+ .reject(&:empty?)
112
+ end
113
+
114
+ def detached_blank(slice)
115
+ slice.last == "" ? [ slice[0...-1], [ "" ] ] : [ slice ]
116
+ end
117
+
118
+ def filled(paragraph)
119
+ words = paragraph.join(" ").split
120
+ words.any? ? wrapped(words).map { "# #{it}" } : [ "#" ]
121
+ end
122
+
123
+ def wrapped(words)
124
+ words.each_with_object([]) do |word, filled|
125
+ if filled.any? && width_of("#{filled.last} #{word}") <= max_length
126
+ filled[-1] = "#{filled.last} #{word}"
127
+ else
128
+ filled << word
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,106 @@
1
+ # Detects a fixture called in several tests of one file. Each test then repeats
2
+ # the line naming the record, and a reader learns which record the file is
3
+ # about only by comparing them, where one `setup` says it once and every test
4
+ # reads it from there.
5
+ #
6
+ # @example
7
+ # # bad - same fixture in multiple tests
8
+ # test "validates name" do
9
+ # user = users(:john)
10
+ # assert user.valid?
11
+ # end
12
+ #
13
+ # test "validates email" do
14
+ # user = users(:john)
15
+ # assert user.email.present?
16
+ # end
17
+ #
18
+ # # good - fixture in setup
19
+ # setup do
20
+ # @user = users(:john)
21
+ # end
22
+ #
23
+ # test "validates name" do
24
+ # assert @user.valid?
25
+ # end
26
+ #
27
+ # test "validates email" do
28
+ # assert @user.email.present?
29
+ # end
30
+ #
31
+ class RuboCop::Cop::Callbacksystems::RepeatedFixtureInTests < RuboCop::Cop::Callbacksystems::Base
32
+ include RuboCop::Callbacksystems::Testing::CopHelpers
33
+
34
+ def external_dependency_checksum
35
+ RuboCop::Callbacksystems::Source::FilesChecksum.for \
36
+ "**/test/fixtures/**/*.yml", root: project_root, include_contents: false
37
+ end
38
+
39
+ def on_new_investigation
40
+ tests_by_class.each_value { report_each RepeatedFixtures.new(it, test_blocks:) }
41
+ end
42
+
43
+ private
44
+ def tests_by_class
45
+ test_blocks.each_with_object({}.compare_by_identity) do |test, groups|
46
+ (groups[test_domain_of(test)] ||= []) << test
47
+ end
48
+ end
49
+
50
+ def test_domain_of(test)
51
+ RuboCop::Callbacksystems::Methods::Domain.new(test).container || processed_source.ast
52
+ end
53
+
54
+ class RepeatedFixtures
55
+ include RuboCop::Callbacksystems::Helpers
56
+
57
+ MESSAGE = "Fixture `%<fixture>s` is used in multiple tests. Move it to `setup`."
58
+
59
+ def initialize(tests, test_blocks:)
60
+ @tests = tests
61
+ @test_block_index = {}.compare_by_identity
62
+ test_blocks.each { @test_block_index[it] = true }
63
+ end
64
+
65
+ def each_offense
66
+ repeated_calls.each { yield RuboCop::Callbacksystems::Offense.new(it.node, message_for(it)) }
67
+ end
68
+
69
+ private
70
+ attr_reader :tests, :test_block_index
71
+
72
+ def repeated_calls
73
+ calls.group_by(&:identity).values.select { spans_several_tests?(it) }.flat_map { beyond_first_test(it) }
74
+ end
75
+
76
+ def calls
77
+ tests.flat_map { calls_in(it) }
78
+ end
79
+
80
+ def calls_in(test)
81
+ RuboCop::Callbacksystems::Execution::Immediate.new(test.body, deferred_blocks: nested_tests_in(test))
82
+ .nodes_of_type(:send)
83
+ .map { RuboCop::Callbacksystems::Testing::FixtureCall.new(it) }.select(&:valid?).map { Call.new(test, it) }
84
+ end
85
+
86
+ def nested_tests_in(test)
87
+ nodes_in(test.body, :any_block).select { test_block_index.key?(it) }
88
+ end
89
+
90
+ def spans_several_tests?(calls)
91
+ calls.map(&:test).uniq(&:object_id).many?
92
+ end
93
+
94
+ def beyond_first_test(calls)
95
+ calls.reject { it.test.equal?(calls.first.test) }
96
+ end
97
+
98
+ def message_for(call)
99
+ format(MESSAGE, fixture: call.signature)
100
+ end
101
+
102
+ class Call < Data.define(:test, :fixture)
103
+ delegate :identity, :node, :signature, to: :fixture
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,85 @@
1
+ # Restricts controller actions to the seven standard Rails actions. A custom
2
+ # action is a verb bolted onto a controller, so the controllers stop reading the
3
+ # same way and routes grow one-off entries. The verb turned into a noun gets a
4
+ # controller of its own, `Users::ActivationsController#create`, with the same
5
+ # shape as every other one.
6
+ #
7
+ # Only classes inheriting from a controller are read. A concern is a module,
8
+ # and its methods are not actions.
9
+ #
10
+ # @example
11
+ # # bad - custom public action
12
+ # class UsersController < ApplicationController
13
+ # def activate
14
+ # # ...
15
+ # end
16
+ # end
17
+ #
18
+ # # good - use standard actions
19
+ # class UsersController < ApplicationController
20
+ # def update
21
+ # # ...
22
+ # end
23
+ # end
24
+ #
25
+ # # good - create a new controller for the action
26
+ # class Users::ActivationsController < ApplicationController
27
+ # def create
28
+ # # ...
29
+ # end
30
+ # end
31
+ #
32
+ # # good - concerns can have any methods
33
+ # module Authentication
34
+ # def current_user
35
+ # # ...
36
+ # end
37
+ # end
38
+ #
39
+ class RuboCop::Cop::Callbacksystems::RestrictedControllerActions < RuboCop::Cop::Callbacksystems::Base
40
+ def on_def(node)
41
+ report Action.new(node)
42
+ end
43
+
44
+ private
45
+ class Action
46
+ include RuboCop::Callbacksystems::Helpers
47
+
48
+ MESSAGE = "Only standard Rails actions (index, show, new, create, edit, update, destroy) are allowed in " \
49
+ "controllers. Extract `%<method>s` to a new controller."
50
+
51
+ def initialize(node)
52
+ @node = node
53
+ end
54
+
55
+ def offense
56
+ RuboCop::Callbacksystems::Offense.new(node, message) if restricted?
57
+ end
58
+
59
+ private
60
+ attr_reader :node
61
+
62
+ def restricted?
63
+ custom? && direct_method_definition?(node) && instance_method? && in_restricted_controller? &&
64
+ public_method?(node)
65
+ end
66
+
67
+ def custom?
68
+ STANDARD_CONTROLLER_ACTIONS.exclude?(node.method_name)
69
+ end
70
+
71
+ def instance_method?
72
+ RuboCop::Callbacksystems::Methods::Domain.new(node).scope == :instance
73
+ end
74
+
75
+ def in_restricted_controller?
76
+ enclosing_class_or_module_of(node).then do |controller|
77
+ controller_class?(controller) && !application_controller_class?(controller)
78
+ end
79
+ end
80
+
81
+ def message
82
+ format(MESSAGE, method: node.method_name)
83
+ end
84
+ end
85
+ end