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,478 @@
1
+ # Prefers backslash line continuation over parentheses for a call that `Layout/LineLength` keeps off a single line.
2
+ # The backslash leaves the arguments as a plain list below the call, one per line and with nothing to close them,
3
+ # which is how a long call reads best. A call that would fit on one line belongs there instead, which
4
+ # `Callbacksystems/CollapseMultilineExpression` asks for, so this one stays quiet about it.
5
+ #
6
+ # @example
7
+ # # bad - multiline with parentheses
8
+ # claim = Account::Invitation::Claim.new(
9
+ # invitation: invitations(:bruno),
10
+ # name: "Test User"
11
+ # )
12
+ #
13
+ # # good - multiline with backslash
14
+ # claim = Account::Invitation::Claim.new \
15
+ # invitation: invitations(:bruno),
16
+ # name: "Test User"
17
+ #
18
+ # # good - short enough for one line
19
+ # claim = Claim.new(
20
+ # name: "Test User"
21
+ # )
22
+ #
23
+ # # bad - first argument left up on the opening line
24
+ # preference = Payment::Preference.create(client,
25
+ # items: items)
26
+ #
27
+ # # good - every argument below the backslash
28
+ # preference = Payment::Preference.create \
29
+ # client,
30
+ # items: items
31
+ #
32
+ # # good - method call without parentheses
33
+ # redirect_to account_path, notice: t(".success")
34
+ #
35
+ # # good - nested call (a backslash inside parentheses would take the outer commas)
36
+ # update!(time_block: Model.find_or_create_by!(
37
+ # starts_at: starts_at, ends_at: ends_at
38
+ # ))
39
+ #
40
+ # # bad - the last argument of a bare command, whose commas can only be its own
41
+ # get availability_path(
42
+ # line_item, rule_id: rule.id, sid: cart.signed_id
43
+ # )
44
+ #
45
+ # # good
46
+ # get availability_path \
47
+ # line_item, rule_id: rule.id, sid: cart.signed_id
48
+ #
49
+ # # good - block disambiguation (without parens block goes to outer method)
50
+ # concat(tag.div do
51
+ # content
52
+ # end)
53
+ #
54
+ # # good - a call handing over a block
55
+ # link_to(notification,
56
+ # class: classes,
57
+ # &)
58
+ #
59
+ # # good - an argument runs past the opening line
60
+ # selections.add(rule: rule, params: {
61
+ # starts_at: starts_at
62
+ # })
63
+ #
64
+ class RuboCop::Cop::Callbacksystems::PreferBackslashContinuation < RuboCop::Cop::Callbacksystems::Base
65
+ extend RuboCop::Cop::AutoCorrector
66
+
67
+ def on_new_investigation
68
+ @source_comments = RuboCop::Callbacksystems::Source::Comments.for(processed_source)
69
+ @structural_contents = StructuralContents.new(processed_source.ast)
70
+ end
71
+
72
+ def on_send(node)
73
+ report MethodCall.new(node, processed_source, source_comments:, structural_contents:, max_line_length:)
74
+ end
75
+
76
+ alias on_csend on_send
77
+
78
+ private
79
+ attr_reader :source_comments, :structural_contents
80
+
81
+ # Structural facts indexed bottom-up once, rather than rediscovering a nested call or block from every ancestor.
82
+ class StructuralContents
83
+ def initialize(root)
84
+ @root = root
85
+ end
86
+
87
+ def multiline_call_within?(node)
88
+ multiline_calls.within?(node)
89
+ end
90
+
91
+ def block_within?(node)
92
+ blocks.within?(node)
93
+ end
94
+
95
+ def forwarded_arguments_in?(node)
96
+ forwarded_arguments.including?(node)
97
+ end
98
+
99
+ private
100
+ attr_reader :root
101
+
102
+ def multiline_calls
103
+ @multiline_calls ||= Descendants.new(root, :multiline_call)
104
+ end
105
+
106
+ def blocks
107
+ @blocks ||= Descendants.new(root, :block)
108
+ end
109
+
110
+ def forwarded_arguments
111
+ @forwarded_arguments ||= Descendants.new(root, :forwarded_argument)
112
+ end
113
+
114
+ # One kind of structural node and the ancestors whose subtrees contain it.
115
+ class Descendants
116
+ include RuboCop::Callbacksystems::Helpers
117
+
118
+ def initialize(root, kind)
119
+ @root = root
120
+ @kind = kind
121
+ end
122
+
123
+ def within?(node)
124
+ node.each_child_node.any? { including?(it) }
125
+ end
126
+
127
+ def including?(node)
128
+ matching_subtrees.key?(node)
129
+ end
130
+
131
+ private
132
+ attr_reader :root, :kind
133
+
134
+ def matching_subtrees
135
+ @matching_subtrees ||= nodes_in(root).reverse_each.with_object({}.compare_by_identity) do |node, matches|
136
+ matches[node] = true if matching_node?(node) || node.each_child_node.any? { matches.key?(it) }
137
+ end
138
+ end
139
+
140
+ def matching_node?(node)
141
+ case kind
142
+ when :multiline_call then multiline_send?(node)
143
+ when :block then any_block_type?(node)
144
+ when :forwarded_argument then node.type?(:forwarded_restarg, :forwarded_kwrestarg)
145
+ else false
146
+ end
147
+ end
148
+
149
+ def multiline_send?(node)
150
+ node.type?(:send, :csend) && node.arguments? && node.loc.begin && node.loc.end &&
151
+ !same_line?(node.loc.begin, node.loc.end)
152
+ end
153
+ end
154
+ end
155
+
156
+ class MethodCall
157
+ include RuboCop::Callbacksystems::Helpers
158
+
159
+ MESSAGE = "Use `\\` for line continuation instead of wrapping arguments in parentheses."
160
+ INDENTATION = " "
161
+ NOTHING_BUT_THE_PARENTHESIS = /\A[\s,]*\)\z/
162
+ COMMA_AFTER_ARGUMENTS = /\A[ \t]*,/
163
+
164
+ def initialize(node, processed_source, source_comments:, structural_contents:, max_line_length:)
165
+ @node = node
166
+ @processed_source = processed_source
167
+ @source_comments = source_comments
168
+ @structural_contents = structural_contents
169
+ @max_line_length = max_line_length
170
+ end
171
+
172
+ def offense
173
+ if wrapped_in_parentheses?
174
+ RuboCop::Callbacksystems::Offense.new(node.loc.begin, MESSAGE, correcting: correctable?) { correct(it) }
175
+ end
176
+ end
177
+
178
+ private
179
+ attr_reader :node, :processed_source, :source_comments, :structural_contents, :max_line_length
180
+
181
+ def wrapped_in_parentheses?
182
+ node.parenthesized? && multiline_send?(node) && !allowed?
183
+ end
184
+
185
+ def multiline_send?(send_node)
186
+ send_node.arguments? && send_node.loc.begin && send_node.loc.end &&
187
+ !same_line?(send_node.loc.begin, send_node.loc.end)
188
+ end
189
+
190
+ def allowed?
191
+ belongs_on_one_line? || braces_still_settling? || inside_other_expression? ||
192
+ unconvertible_arguments? || carries_a_block? || opening_comment_destination_occupied?
193
+ end
194
+
195
+ def belongs_on_one_line?
196
+ RuboCop::Callbacksystems::Source::SingleLineCall.new(node, max_line_length).fits?
197
+ end
198
+
199
+ # `Layout/MultilineMethodCallBraceLayout` still moves that parenthesis, and two rewrites in one pass break it.
200
+ def braces_still_settling?
201
+ first_argument_on_opening_line? && parenthesis_alone_on_its_line?
202
+ end
203
+
204
+ def first_argument_on_opening_line?
205
+ first_argument_line == opening_line
206
+ end
207
+
208
+ def first_argument_line
209
+ node.first_argument.first_line
210
+ end
211
+
212
+ def opening_line
213
+ node.loc.begin.line
214
+ end
215
+
216
+ def parenthesis_alone_on_its_line?
217
+ node.loc.end.source_line.strip == ")"
218
+ end
219
+
220
+ def inside_other_expression?
221
+ enclosing_syntax.commas_claimed_by_a_list? || inside_backslash_continuation? ||
222
+ enclosing_syntax.part_of_a_larger_expression?
223
+ end
224
+
225
+ def enclosing_syntax
226
+ @enclosing_syntax ||= EnclosingSyntax.new(node)
227
+ end
228
+
229
+ def inside_backslash_continuation?
230
+ previous_line(node.loc.begin.line)&.rstrip&.end_with?("\\")
231
+ end
232
+
233
+ def previous_line(line)
234
+ processed_source.lines[line - 2] if line > 1
235
+ end
236
+
237
+ def unconvertible_arguments?
238
+ argument_has_block? || contains_multiline_call? || leading_braced_hash? ||
239
+ opening_line_ends_inside_an_argument? || control_flow_argument? || forwards_anonymous_arguments?
240
+ end
241
+
242
+ def argument_has_block?
243
+ direct_block_argument? || nested_block_in_arguments?
244
+ end
245
+
246
+ def direct_block_argument?
247
+ node.arguments.any? { hands_over_a_block?(it) }
248
+ end
249
+
250
+ # Ruby reads a `&` as ambiguous when nothing precedes it on the line, which is where a continuation puts it.
251
+ def hands_over_a_block?(argument)
252
+ any_block_type?(argument) || argument.block_pass_type?
253
+ end
254
+
255
+ def nested_block_in_arguments?
256
+ node.arguments.any? { structural_contents.block_within?(it) }
257
+ end
258
+
259
+ def contains_multiline_call?
260
+ structural_contents.multiline_call_within?(node)
261
+ end
262
+
263
+ def leading_braced_hash?
264
+ node.first_argument.source.start_with?("{")
265
+ end
266
+
267
+ # The lines such an argument spans are indented against a parenthesis that is going away.
268
+ def opening_line_ends_inside_an_argument?
269
+ node.arguments.any? { it.first_line == opening_line && it.last_line > opening_line }
270
+ end
271
+
272
+ def control_flow_argument?
273
+ node.arguments.any? { it.type?(:if, :case, :case_match, :while, :until) }
274
+ end
275
+
276
+ def forwards_anonymous_arguments?
277
+ node.arguments.any? { structural_contents.forwarded_arguments_in?(it) }
278
+ end
279
+
280
+ def carries_a_block?
281
+ any_block_type?(node.parent) && node.parent.send_node.equal?(node)
282
+ end
283
+
284
+ # The opening comment normally moves after the last argument. If that position already carries a comment,
285
+ # combining them would turn two comments into one and standing either between the continuation lines breaks it.
286
+ def opening_comment_destination_occupied?
287
+ opening_comment && follows_last_argument?(source_comments.trailing_comment_on(last_argument_end.line))
288
+ end
289
+
290
+ def opening_comment
291
+ @opening_comment ||= source_comments.trailing_comment_on(opening_line)&.then { it if follows_opening?(it) }
292
+ end
293
+
294
+ def follows_opening?(comment)
295
+ same_line?(comment, node.loc.begin) && node.loc.begin.end.join(comment.source_range.begin).source.blank?
296
+ end
297
+
298
+ def follows_last_argument?(comment)
299
+ comment && comment.loc.line == last_argument_end.line &&
300
+ comment.source_range.begin_pos >= last_argument_end.begin_pos
301
+ end
302
+
303
+ # The opener of a heredoc can take a comment, its terminator cannot.
304
+ def last_argument_end
305
+ node.last_argument.source_range.end
306
+ end
307
+
308
+ def correctable?
309
+ opening_comment_movable? && arguments_uncommented? &&
310
+ (parenthesis_alone_on_its_line? || (closing_parenthesis_stands_alone? && comments_undisturbed?))
311
+ end
312
+
313
+ def opening_comment_movable?
314
+ opening_comment.nil? || !tooling_comment?(opening_comment)
315
+ end
316
+
317
+ # After a backslash the next line is joined to the call, so a comment above the first argument would swallow it.
318
+ def arguments_uncommented?
319
+ !source_comments.any_on_lines?((node.loc.begin.line + 1)..(first_argument_line - 1))
320
+ end
321
+
322
+ def closing_parenthesis_stands_alone?
323
+ trailing_parenthesis_range.source.match?(NOTHING_BUT_THE_PARENTHESIS)
324
+ end
325
+
326
+ def trailing_parenthesis_range
327
+ arguments_end.join(node.loc.end)
328
+ end
329
+
330
+ def arguments_end
331
+ node.arguments.map { range_through_heredocs(it) }.max_by(&:end_pos).end
332
+ end
333
+
334
+ def comments_undisturbed?
335
+ !orphaned?(source_comments.trailing_comment_on(node.loc.end.line))
336
+ end
337
+
338
+ def orphaned?(comment)
339
+ comment && same_line?(comment, node.loc.end) && comment.source_range.begin_pos >= node.loc.end.end_pos
340
+ end
341
+
342
+ def correct(corrector)
343
+ corrector.replace(opening, continuation)
344
+ expand_trailing_shorthand(corrector)
345
+ corrector.insert_after(last_argument_end, " #{opening_comment.text}") if opening_comment
346
+ removals.each { corrector.remove(it) }
347
+ end
348
+
349
+ # A comment written right after the parenthesis cannot follow a backslash, so it closes the arguments instead.
350
+ def opening
351
+ opening_comment ? node.loc.begin.join(opening_comment.source_range) : node.loc.begin
352
+ end
353
+
354
+ def continuation
355
+ first_argument_on_opening_line? ? " \\\n#{argument_indentation}" : " \\"
356
+ end
357
+
358
+ def argument_indentation
359
+ node.loc.begin.source_line[/\A\s*/] + INDENTATION
360
+ end
361
+
362
+ # Without the closing parenthesis, a final `option:` can take the expression on the next line as its value.
363
+ def expand_trailing_shorthand(corrector)
364
+ trailing_shorthand&.then { corrector.insert_after(it, " #{it.value.source}") }
365
+ end
366
+
367
+ def trailing_shorthand
368
+ trailing_pair if trailing_pair&.value_omission?
369
+ end
370
+
371
+ def trailing_pair
372
+ @trailing_pair ||= node.last_argument.then { it.pairs.last if it.hash_type? }
373
+ end
374
+
375
+ # Taking the whole line leaves a comment above the parenthesis where it was, which cutting back would not.
376
+ def removals
377
+ parenthesis_alone_on_its_line? ? [ trailing_comma, parenthesis_line ].compact : [ trailing_parenthesis_range ]
378
+ end
379
+
380
+ # The parenthesis is what kept a trailing comma legal, so it goes along.
381
+ def trailing_comma
382
+ match = COMMA_AFTER_ARGUMENTS.match(trailing_parenthesis_range.source)
383
+ arguments_end.adjust(end_pos: match.to_s.length) if match
384
+ end
385
+
386
+ def parenthesis_line
387
+ line_removal_range_of(node.loc.end)
388
+ end
389
+ end
390
+
391
+ # The ancestor syntax that would claim the commas or make a bare continued call parse differently.
392
+ class EnclosingSyntax
393
+ include RuboCop::Callbacksystems::Helpers
394
+
395
+ def initialize(node)
396
+ @node = node
397
+ end
398
+
399
+ def commas_claimed_by_a_list?
400
+ nested_in_call? || inside_parameter_list? || inside_collection_literal? || matched_by_a_when?
401
+ end
402
+
403
+ def part_of_a_larger_expression?
404
+ chained_method_receiver? || operand_of_boolean? || branch_of_one_line_conditional? ||
405
+ assigned_inside_a_condition? || endpoint_of_range? || matched_as_a_pattern? || constant_namespace?
406
+ end
407
+
408
+ private
409
+ attr_reader :node
410
+
411
+ def nested_in_call?
412
+ enclosing_calls.any? { !last_argument_of_bare_command?(it) }
413
+ end
414
+
415
+ def enclosing_calls
416
+ node.each_ancestor(:send, :csend, :super, :yield).select { |call| call.arguments.any? { holds?(it) } }
417
+ end
418
+
419
+ def holds?(other)
420
+ other.equal?(node) || ancestor_nodes.key?(other)
421
+ end
422
+
423
+ def ancestor_nodes
424
+ @ancestor_nodes ||= {}.compare_by_identity.tap do |ancestors|
425
+ node.each_ancestor { ancestors[it] = true }
426
+ end
427
+ end
428
+
429
+ # A bare command ending in this call leaves the commas to it, so only there the backslash reads right.
430
+ def last_argument_of_bare_command?(call)
431
+ !call.parenthesized? && call.last_argument.equal?(node)
432
+ end
433
+
434
+ def inside_parameter_list?
435
+ node.each_ancestor(:args).any?
436
+ end
437
+
438
+ def inside_collection_literal?
439
+ node.each_ancestor(:hash, :array).any?
440
+ end
441
+
442
+ def matched_by_a_when?
443
+ node.each_ancestor(:when).any? { |branch| branch.conditions.any? { holds?(it) } }
444
+ end
445
+
446
+ def chained_method_receiver?
447
+ node.parent&.call_type? && node.parent.receiver == node
448
+ end
449
+
450
+ def operand_of_boolean?
451
+ node.each_ancestor(:and, :or).any?
452
+ end
453
+
454
+ def branch_of_one_line_conditional?
455
+ node.each_ancestor(:if).any? { it.ternary? || it.modifier_form? }
456
+ end
457
+
458
+ # Ruby rejects a call without parentheses as the value of an assignment that a condition tests.
459
+ def assigned_inside_a_condition?
460
+ node.each_ancestor(:if, :while, :until).any? { it.condition.assignment? && holds?(it.condition) }
461
+ end
462
+
463
+ # A command-style call consumes a following range operator as part of its final argument.
464
+ def endpoint_of_range?
465
+ node.each_ancestor(:irange, :erange).any?
466
+ end
467
+
468
+ # The command form would make `=>` or `in` bind to the final argument rather than the call's result.
469
+ def matched_as_a_pattern?
470
+ node.each_ancestor(:match_pattern, :match_pattern_p).any?
471
+ end
472
+
473
+ # The command form would make `::Name` qualify the final argument instead of the call's result.
474
+ def constant_namespace?
475
+ node.each_ancestor(:const).any? { holds?(it.namespace) }
476
+ end
477
+ end
478
+ end