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,52 @@
1
+ # Keeps Turbo Stream markup out of controllers. A call on the `turbo_stream`
2
+ # tag builder renders elements, and markup belongs to the view, where the same
3
+ # `turbo_stream.replace` reads as a template while the controller only decides
4
+ # what to render and with which status.
5
+ #
6
+ # @example
7
+ # # bad - the controller builds the stream
8
+ # class MessagesController < ApplicationController
9
+ # def update
10
+ # if @message.update(message_params)
11
+ # redirect_to @message
12
+ # else
13
+ # render turbo_stream: turbo_stream.replace(@message, partial: "messages/form")
14
+ # end
15
+ # end
16
+ # end
17
+ #
18
+ # # good - the controller renders and the view builds the stream
19
+ # class MessagesController < ApplicationController
20
+ # def update
21
+ # if @message.update(message_params)
22
+ # redirect_to @message
23
+ # else
24
+ # respond_to do |format|
25
+ # format.turbo_stream { render status: :unprocessable_content }
26
+ # end
27
+ # end
28
+ # end
29
+ # end
30
+ #
31
+ # # app/views/messages/update.turbo_stream.erb
32
+ # <%= turbo_stream.replace @message, partial: "messages/form" %>
33
+ #
34
+ class RuboCop::Cop::Callbacksystems::NoTurboStreamInControllers < RuboCop::Cop::Callbacksystems::Base
35
+ MESSAGE = "Build the Turbo Stream in a `.turbo_stream.erb` view instead of calling `turbo_stream.%<method>s` in " \
36
+ "the controller."
37
+
38
+ def on_send(node)
39
+ add_offense(node, message: format(MESSAGE, method: node.method_name)) if turbo_stream_builder_call?(node)
40
+ end
41
+
42
+ alias on_csend on_send
43
+
44
+ private
45
+ def turbo_stream_builder_call?(node)
46
+ controller_file?(processed_source.file_path) && turbo_stream_builder?(node.receiver)
47
+ end
48
+
49
+ def turbo_stream_builder?(receiver)
50
+ call_on_self?(receiver) && receiver.method?(:turbo_stream)
51
+ end
52
+ end
@@ -0,0 +1,159 @@
1
+ # AI agents (and pasting from formatted text) sneak typographic characters into
2
+ # source: smart quotes, dashes, ellipses, invisible spaces. What counts as
3
+ # clutter depends on where the character sits.
4
+ #
5
+ # A comment only describes the code, so a typographic spelling of punctuation is
6
+ # noise there: it breaks grep and reads as sloppy review. A string shows the text
7
+ # it holds, so a character a reader can see is content someone wrote on purpose.
8
+ # Only the invisible marks are clutter inside a string, since those change what
9
+ # the string compares as without showing it, so an equality that reads as true
10
+ # comes out false and a `split` misses where you meant it to cut.
11
+ #
12
+ # Arrows, bullets, check marks and guillemets are left alone in both places. They
13
+ # carry meaning of their own and no ASCII spelling replaces them.
14
+ #
15
+ # The characters below are written as escapes, since this file is checked too.
16
+ #
17
+ # @example
18
+ # # bad - an em dash in a comment
19
+ # # name \u2014 required
20
+ # name = params[:name]
21
+ #
22
+ # # bad - a double hyphen (two `-` in a row) standing in for a dash
23
+ # # name \u002D\u002D required
24
+ #
25
+ # # bad - a no-break space in a string, which compares unlike a plain space
26
+ # label = "first\u00A0name"
27
+ #
28
+ # # good - plain ASCII punctuation in the comment
29
+ # # name, required
30
+ # name = params[:name]
31
+ #
32
+ # # good - a visible character left as written inside the string
33
+ # label = "said \u201Cyes\u201D"
34
+ #
35
+ class RuboCop::Cop::Callbacksystems::NoTypographicClutter < RuboCop::Cop::Callbacksystems::Base
36
+ extend RuboCop::Cop::AutoCorrector
37
+
38
+ def on_new_investigation
39
+ prose_comments_in(processed_source).each do |comment|
40
+ report_each Clutter.new(comment.source_range, comment.text, clutter: COMMENT_CLUTTER), rewrites_comments: true
41
+ report_each DoubleHyphens.new(comment), rewrites_comments: true
42
+ end
43
+ string_ranges.each { report_each Clutter.new(it, it.source, clutter: STRING_CLUTTER) }
44
+ end
45
+
46
+ private
47
+ # Marks a reader cannot see: no-break space, soft hyphen, narrow no-break space, word joiner, BOM.
48
+ INVISIBLE_POINTS = [ 0x00A0, 0x00AD, 0x202F, 0x2060, 0xFEFF ]
49
+ COMMENT_CLUTTER = Set[*INVISIBLE_POINTS, 0x2013, 0x2014, 0x2026, 0x2212, *0x200B..0x200D, *0x2018..0x201F]
50
+ # Zero-width marks stay out of strings, since they join an emoji sequence and deleting one splits the emoji.
51
+ STRING_CLUTTER = Set[*INVISIBLE_POINTS]
52
+
53
+ def string_ranges
54
+ nodes_in(processed_source.ast, :str).map { string_range_of(it) }
55
+ end
56
+
57
+ def string_range_of(node)
58
+ node.loc?(:heredoc_body) ? node.loc.heredoc_body : node.source_range
59
+ end
60
+
61
+ class Clutter
62
+ def initialize(range, text, clutter:)
63
+ @range = range
64
+ @text = text
65
+ @clutter = clutter
66
+ end
67
+
68
+ def each_offense
69
+ occurrences.each { yield it.offense }
70
+ end
71
+
72
+ private
73
+ attr_reader :range, :text, :clutter
74
+
75
+ def occurrences
76
+ text.each_char.with_index.filter_map do |character, offset|
77
+ Occurrence.new(range, character, offset) if clutter.include?(character.ord)
78
+ end
79
+ end
80
+
81
+ class Occurrence
82
+ MESSAGE = "Remove typographic character `%<character>s` (U+%<code_point>s). Use ASCII equivalents."
83
+ # Only characters with one meaning-preserving ASCII spelling, keyed by escape so this file passes its own cop.
84
+ ASCII_EQUIVALENT = {
85
+ "\u2018" => "'", "\u2019" => "'",
86
+ "\u201C" => "\"", "\u201D" => "\"",
87
+ "\u2026" => "...",
88
+ "\u00A0" => " ", "\u202F" => " ",
89
+ "\u200B" => "", "\u200C" => "", "\u200D" => "", "\u2060" => "", "\uFEFF" => "",
90
+ "\u00AD" => ""
91
+ }
92
+
93
+ def initialize(source_range, character, offset)
94
+ @source_range = source_range
95
+ @character = character
96
+ @offset = offset
97
+ end
98
+
99
+ def offense
100
+ RuboCop::Callbacksystems::Offense.new(range, message, correcting: !replacement.nil?) { correct(it) }
101
+ end
102
+
103
+ private
104
+ attr_reader :source_range, :character, :offset
105
+
106
+ def range
107
+ source_range.with(begin_pos: source_range.begin_pos + offset, end_pos: range_end)
108
+ end
109
+
110
+ def range_end
111
+ source_range.begin_pos + offset + character.length
112
+ end
113
+
114
+ def message
115
+ format(MESSAGE, character: character, code_point: code_point)
116
+ end
117
+
118
+ def code_point
119
+ format("%04X", character.ord)
120
+ end
121
+
122
+ def replacement
123
+ ASCII_EQUIVALENT[character]
124
+ end
125
+
126
+ def correct(corrector)
127
+ corrector.replace(range, replacement)
128
+ end
129
+ end
130
+ end
131
+
132
+ # Double hyphens standing in for dashes. Which punctuation replaces one depends on the sentence, so there is no fix.
133
+ class DoubleHyphens
134
+ MESSAGE = "Avoid `--` in comments; use a comma, colon, or parentheses."
135
+
136
+ def initialize(comment)
137
+ @comment = comment
138
+ end
139
+
140
+ def each_offense
141
+ offsets.each { yield RuboCop::Callbacksystems::Offense.new(range_at(it), MESSAGE) }
142
+ end
143
+
144
+ private
145
+ attr_reader :comment
146
+
147
+ def offsets
148
+ comment.text.enum_for(:scan, /--/).map { Regexp.last_match.begin(0) }
149
+ end
150
+
151
+ def range_at(offset)
152
+ comment.source_range.with(begin_pos: comment.source_range.begin_pos + offset, end_pos: range_end(offset))
153
+ end
154
+
155
+ def range_end(offset)
156
+ comment.source_range.begin_pos + offset + 2
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,149 @@
1
+ # Keeps GET actions free of writes and enqueued side effects. HTTP promises a
2
+ # GET changes nothing, and browsers, crawlers and link previews act on that
3
+ # promise by prefetching, retrying and replaying it, so a write in `show` runs
4
+ # as many times as the page is opened and by clients that never meant to act.
5
+ # Only applies to index, show, new, and edit in controller classes, and only
6
+ # to calls made directly in the action body, so a write pushed into a model
7
+ # method stays a deliberate choice.
8
+ #
9
+ # @example
10
+ # # bad - GET action persists a change
11
+ # class MessagesController < ApplicationController
12
+ # def show
13
+ # @message = Message.find(params[:id])
14
+ # @message.update!(read_at: Time.current)
15
+ # end
16
+ # end
17
+ #
18
+ # # bad - GET action enqueues a side effect
19
+ # class MessagesController < ApplicationController
20
+ # def show
21
+ # ReadReceiptJob.perform_later(params[:id])
22
+ # end
23
+ # end
24
+ #
25
+ # # good - the write moves behind a non-GET action
26
+ # class Messages::ReadingsController < ApplicationController
27
+ # def create
28
+ # Message.find(params[:message_id]).update!(read_at: Time.current)
29
+ # end
30
+ # end
31
+ #
32
+ class RuboCop::Cop::Callbacksystems::NoWritesInGetActions < RuboCop::Cop::Callbacksystems::Base
33
+ def on_def(node)
34
+ report_each GetAction.new(node)
35
+ end
36
+
37
+ private
38
+ class GetAction
39
+ include RuboCop::Callbacksystems::Helpers
40
+
41
+ MESSAGE = "`%<action>s` serves GET requests, so it must not call `%<method>s`. Move the write behind a non-GET " \
42
+ "action."
43
+ GET_ACTIONS = %i[ index show new edit ]
44
+ WRITE_METHODS = %i[
45
+ create create! save save! update update! update_attribute update_column update_columns
46
+ destroy destroy! destroy_all delete delete_all update_all touch increment! decrement! toggle!
47
+ find_or_create_by find_or_create_by! create_or_find_by create_or_find_by!
48
+ first_or_create first_or_create! increment_counter decrement_counter update_counters
49
+ insert_all insert_all! upsert upsert_all deliver_now perform_now
50
+ ]
51
+ DEFERRED_SUFFIX = "_later"
52
+ REQUEST_STATE_RECEIVERS = %i[ session cookies flash params headers request response ]
53
+
54
+ def initialize(node)
55
+ @node = node
56
+ end
57
+
58
+ def each_offense
59
+ writes.each { yield RuboCop::Callbacksystems::Offense.new(it, message_for(it)) }
60
+ end
61
+
62
+ private
63
+ attr_reader :node
64
+
65
+ def writes
66
+ if get_action?
67
+ node.each_node(:call).select { write?(it) && !inside_deferred_callable?(it) }
68
+ else
69
+ []
70
+ end
71
+ end
72
+
73
+ def get_action?
74
+ GET_ACTIONS.include?(node.method_name) && direct_method_definition?(node) && instance_method? &&
75
+ enclosing_controller_class? && public_method?(node)
76
+ end
77
+
78
+ def instance_method?
79
+ RuboCop::Callbacksystems::Methods::Domain.new(node).scope == :instance
80
+ end
81
+
82
+ def enclosing_controller_class?
83
+ controller_class?(enclosing_class_or_module_of(node))
84
+ end
85
+
86
+ def write?(call)
87
+ write_name?(call.method_name) && !RequestStateReceiver.new(call.receiver).request_state?
88
+ end
89
+
90
+ def write_name?(name)
91
+ WRITE_METHODS.include?(name) || name.end_with?(DEFERRED_SUFFIX)
92
+ end
93
+
94
+ def inside_deferred_callable?(call)
95
+ call.each_ancestor.take_while { !it.equal?(node) }.any? do |ancestor|
96
+ ancestor.any_def_type? || deferred_callable_block?(ancestor)
97
+ end
98
+ end
99
+
100
+ def message_for(write)
101
+ format(MESSAGE, action: node.method_name, method: write.method_name)
102
+ end
103
+
104
+ class RequestStateReceiver
105
+ def initialize(receiver)
106
+ @receiver = receiver
107
+ end
108
+
109
+ def request_state?
110
+ ReceiverPath.new(receiver).any? { request_state_reader?(it) }
111
+ end
112
+
113
+ private
114
+ attr_reader :receiver
115
+
116
+ def request_state_reader?(candidate)
117
+ candidate.call_type? && REQUEST_STATE_RECEIVERS.include?(candidate.method_name) &&
118
+ (candidate.receiver.nil? || candidate.receiver.self_type?)
119
+ end
120
+
121
+ class ReceiverPath
122
+ include Enumerable
123
+
124
+ def initialize(receiver)
125
+ @receiver = receiver
126
+ end
127
+
128
+ def each
129
+ if block_given?
130
+ current = receiver
131
+ until current.nil?
132
+ yield current
133
+ current = if current.begin_type? && current.children.one?
134
+ current.children.first
135
+ elsif current.call_type?
136
+ current.receiver
137
+ end
138
+ end
139
+ else
140
+ to_enum(__method__)
141
+ end
142
+ end
143
+
144
+ private
145
+ attr_reader :receiver
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,78 @@
1
+ # Ensures mixin arguments are sorted alphabetically.
2
+ #
3
+ # When including, extending, or prepending multiple modules on the same line,
4
+ # they should be sorted alphabetically for consistency and easier scanning.
5
+ #
6
+ # The order of one `include` decides which module wins when two define the same
7
+ # method, so sorting can change what the class does. The fix runs under `-A`
8
+ # rather than `-a` for that reason. Write the modules on separate lines when the
9
+ # order between them matters.
10
+ #
11
+ # @example
12
+ # # bad - unsorted
13
+ # include Searchable, Confirmable, Accessible
14
+ #
15
+ # # good - sorted alphabetically
16
+ # include Accessible, Confirmable, Searchable
17
+ #
18
+ # # good - single module (nothing to sort)
19
+ # include Searchable
20
+ #
21
+ # # good - separate lines (each line is independent)
22
+ # include Searchable
23
+ # include Confirmable # depends on Searchable
24
+ #
25
+ class RuboCop::Cop::Callbacksystems::OrderedMixinArguments < RuboCop::Cop::Callbacksystems::Base
26
+ extend RuboCop::Cop::AutoCorrector
27
+
28
+ def on_send(node)
29
+ report MixinCall.new(node, processed_source)
30
+ end
31
+
32
+ alias on_csend on_send
33
+
34
+ private
35
+ class MixinCall
36
+ include RuboCop::Callbacksystems::Helpers
37
+
38
+ MESSAGE = "Sort mixin arguments alphabetically: `%<sorted>s`."
39
+
40
+ def initialize(node, processed_source)
41
+ @node = node
42
+ @processed_source = processed_source
43
+ end
44
+
45
+ def offense
46
+ RuboCop::Callbacksystems::Offense.new(node, message, correcting: uncommented?) { correct(it) } if unsorted?
47
+ end
48
+
49
+ private
50
+ attr_reader :node, :processed_source
51
+ delegate :arguments, to: :node, private: true
52
+
53
+ def unsorted?
54
+ mixin_macro?(node) && arguments.many? && names != sorted_names
55
+ end
56
+
57
+ def names
58
+ @names ||= arguments.map(&:source)
59
+ end
60
+
61
+ def sorted_names
62
+ @sorted_names ||= names.sort
63
+ end
64
+
65
+ def message
66
+ format(MESSAGE, sorted: sorted_names.join(", "))
67
+ end
68
+
69
+ # Once arguments move, a comment on their lines no longer names the element or precedence it was written for.
70
+ def uncommented?
71
+ processed_source.comments.none? { it.loc.line.between?(node.first_line, node.last_line) }
72
+ end
73
+
74
+ def correct(corrector)
75
+ corrector.replace(range_spanning(arguments), sorted_names.join(", "))
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,103 @@
1
+ # Ensures controller names are plural. A controller stands for the collection
2
+ # its routes address, `/users` and `/users/1` alike, so the plural is the name
3
+ # Rails derives the route and the resource from, and a singular one reads as a
4
+ # model where a reader expects a resource.
5
+ #
6
+ # @example
7
+ # # bad
8
+ # class UserController < ApplicationController
9
+ # end
10
+ #
11
+ # # good
12
+ # class UsersController < ApplicationController
13
+ # end
14
+ #
15
+ # # good - already plural
16
+ # class PeopleController < ApplicationController
17
+ # end
18
+ #
19
+ # # good - ignored names (configurable)
20
+ # class ApplicationController < ActionController::Base
21
+ # end
22
+ #
23
+ # class BaseController < ApplicationController
24
+ # end
25
+ #
26
+ class RuboCop::Cop::Callbacksystems::PluralControllerNames < RuboCop::Cop::Callbacksystems::Base
27
+ def on_class(node)
28
+ report ControllerClass.new(node, ignored_names: cop_config["IgnoredNames"])
29
+ end
30
+
31
+ private
32
+ class ControllerClass
33
+ include RuboCop::Callbacksystems::Helpers
34
+
35
+ MESSAGE = "Controller names should be plural. Use `%<plural>sController` instead of `%<singular>sController`."
36
+
37
+ def initialize(node, ignored_names:)
38
+ @node = node
39
+ @ignored_names = ignored_names
40
+ end
41
+
42
+ def offense
43
+ RuboCop::Callbacksystems::Offense.new(node.loc.name, message) if singular?
44
+ end
45
+
46
+ private
47
+ attr_reader :node, :ignored_names
48
+
49
+ def singular?
50
+ controller_class?(node) && named_controller? && !resource_name.excluded?
51
+ end
52
+
53
+ def named_controller?
54
+ class_name.end_with?("Controller")
55
+ end
56
+
57
+ def class_name
58
+ constant_name_of(node.identifier)
59
+ end
60
+
61
+ def resource_name
62
+ @resource_name ||= ResourceName.new(class_name.delete_suffix("Controller").split("::").last, ignored_names:)
63
+ end
64
+
65
+ def message
66
+ format(MESSAGE, plural: resource_name.plural, singular: resource_name)
67
+ end
68
+ end
69
+
70
+ class ResourceName
71
+ def initialize(name, ignored_names:)
72
+ @name = name
73
+ @ignored_names = ignored_names
74
+ end
75
+
76
+ def excluded?
77
+ ignored? || plural?
78
+ end
79
+
80
+ def to_s
81
+ name
82
+ end
83
+
84
+ def plural
85
+ name.pluralize
86
+ end
87
+
88
+ private
89
+ attr_reader :name, :ignored_names
90
+
91
+ def ignored?
92
+ ignored_names.include?(name)
93
+ end
94
+
95
+ def plural?
96
+ singular != name || singular.pluralize == name
97
+ end
98
+
99
+ def singular
100
+ @singular ||= name.singularize
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,86 @@
1
+ # Detects direct use of instance variables that were assigned in initialize.
2
+ # Reading `@node` where `node` would do ties the method to how the value is
3
+ # stored, so a getter that later computes or memoizes it has to touch every
4
+ # reader. Behind an `attr_reader` the method reads the name and nothing else.
5
+ #
6
+ # This cop only applies to instance variables assigned in the constructor.
7
+ # Instance variables assigned elsewhere (like in controllers) are not flagged.
8
+ #
9
+ # @example
10
+ # # bad - using @node directly when it was set in initialize
11
+ # class Foo
12
+ # def initialize(node)
13
+ # @node = node
14
+ # end
15
+ #
16
+ # def process
17
+ # @node.children
18
+ # end
19
+ # end
20
+ #
21
+ # # good - using attr_reader
22
+ # class Foo
23
+ # def initialize(node)
24
+ # @node = node
25
+ # end
26
+ #
27
+ # def process
28
+ # node.children
29
+ # end
30
+ #
31
+ # private
32
+ # attr_reader :node
33
+ # end
34
+ #
35
+ class RuboCop::Cop::Callbacksystems::PreferAttrReaderOverInstanceVariable < RuboCop::Cop::Callbacksystems::Base
36
+ def on_class(node)
37
+ report_each InstanceVariableReads.new(node)
38
+ end
39
+
40
+ alias on_module on_class
41
+
42
+ private
43
+ class InstanceVariableReads
44
+ include RuboCop::Callbacksystems::Helpers
45
+
46
+ MESSAGE = "Use `%<name>s` (via attr_reader) instead of `@%<name>s` for instance variables assigned in initialize."
47
+
48
+ def initialize(class_node)
49
+ @class_node = class_node
50
+ end
51
+
52
+ def each_offense
53
+ reads_of_constructor_variables.each { yield offense_for(it) }
54
+ end
55
+
56
+ private
57
+ attr_reader :class_node
58
+
59
+ def reads_of_constructor_variables
60
+ reads_outside_constructor.select { constructor_variable_names.include?(it.name) }
61
+ end
62
+
63
+ def reads_outside_constructor
64
+ nodes_in(class_node.body, :ivar).select { instance_method_read?(it) }
65
+ end
66
+
67
+ def instance_method_read?(read)
68
+ method = enclosing_method_of(read)
69
+ method&.def_type? && direct_method_definition?(method) && !method.method?(:initialize) &&
70
+ enclosing_class_or_module_of(read) == class_node && self_preserved_between?(read, boundary: method)
71
+ end
72
+
73
+ def constructor_variable_names
74
+ @constructor_variable_names ||= nodes_in(initialize_method&.body, :ivasgn)
75
+ .select { self_preserved_between?(it, boundary: initialize_method) }.to_set(&:name)
76
+ end
77
+
78
+ def initialize_method
79
+ direct_method_nodes_in(class_node.body).find { it.method?(:initialize) }
80
+ end
81
+
82
+ def offense_for(read)
83
+ RuboCop::Callbacksystems::Offense.new(read, format(MESSAGE, name: name_without_sigil(read.name)))
84
+ end
85
+ end
86
+ end