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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +78 -0
- data/config/default.yml +590 -0
- data/docs/cops.md +111 -0
- data/lib/rubocop/callbacksystems/autocorrection/batch.rb +135 -0
- data/lib/rubocop/callbacksystems/autocorrection/comment_rescue.rb +112 -0
- data/lib/rubocop/callbacksystems/autocorrection/rehearsal.rb +65 -0
- data/lib/rubocop/callbacksystems/autocorrection/rewrite_permit.rb +14 -0
- data/lib/rubocop/callbacksystems/class_structure/builder_assignment.rb +48 -0
- data/lib/rubocop/callbacksystems/class_structure/class_body.rb +50 -0
- data/lib/rubocop/callbacksystems/class_structure/divergence.rb +31 -0
- data/lib/rubocop/callbacksystems/class_structure/node_visibility.rb +82 -0
- data/lib/rubocop/callbacksystems/class_structure/reference_order.rb +36 -0
- data/lib/rubocop/callbacksystems/class_structure/statement_rank.rb +84 -0
- data/lib/rubocop/callbacksystems/class_structure/statement_run.rb +42 -0
- data/lib/rubocop/callbacksystems/class_structure/visibility.rb +84 -0
- data/lib/rubocop/callbacksystems/cops_document.rb +78 -0
- data/lib/rubocop/callbacksystems/execution/deferred_local_variables.rb +138 -0
- data/lib/rubocop/callbacksystems/execution/discarded_expression.rb +45 -0
- data/lib/rubocop/callbacksystems/execution/eager_evaluation_path.rb +73 -0
- data/lib/rubocop/callbacksystems/execution/immediate.rb +71 -0
- data/lib/rubocop/callbacksystems/execution/local_variable_occurrences.rb +112 -0
- data/lib/rubocop/callbacksystems/hashes/key_transform_detection.rb +108 -0
- data/lib/rubocop/callbacksystems/hashes/keyword_options.rb +37 -0
- data/lib/rubocop/callbacksystems/helpers/bodies.rb +150 -0
- data/lib/rubocop/callbacksystems/helpers/builders.rb +103 -0
- data/lib/rubocop/callbacksystems/helpers/comments.rb +56 -0
- data/lib/rubocop/callbacksystems/helpers/corrections.rb +15 -0
- data/lib/rubocop/callbacksystems/helpers/methods.rb +46 -0
- data/lib/rubocop/callbacksystems/helpers/names.rb +65 -0
- data/lib/rubocop/callbacksystems/helpers/node_types.rb +154 -0
- data/lib/rubocop/callbacksystems/helpers/rails.rb +52 -0
- data/lib/rubocop/callbacksystems/helpers/ranges.rb +107 -0
- data/lib/rubocop/callbacksystems/helpers/recursion.rb +29 -0
- data/lib/rubocop/callbacksystems/helpers/variables.rb +44 -0
- data/lib/rubocop/callbacksystems/helpers/visibility.rb +121 -0
- data/lib/rubocop/callbacksystems/helpers.rb +43 -0
- data/lib/rubocop/callbacksystems/methods/calls.rb +38 -0
- data/lib/rubocop/callbacksystems/methods/definitions.rb +218 -0
- data/lib/rubocop/callbacksystems/methods/delegate_macro.rb +144 -0
- data/lib/rubocop/callbacksystems/methods/delegation_target.rb +55 -0
- data/lib/rubocop/callbacksystems/methods/domain.rb +77 -0
- data/lib/rubocop/callbacksystems/methods/implicit_invocations.rb +17 -0
- data/lib/rubocop/callbacksystems/methods/macro_references.rb +238 -0
- data/lib/rubocop/callbacksystems/methods/reference_evidence.rb +74 -0
- data/lib/rubocop/callbacksystems/methods/siblings.rb +24 -0
- data/lib/rubocop/callbacksystems/methods/statement_definitions.rb +68 -0
- data/lib/rubocop/callbacksystems/offense.rb +13 -0
- data/lib/rubocop/callbacksystems/plugin.rb +27 -0
- data/lib/rubocop/callbacksystems/project_index/cache.rb +16 -0
- data/lib/rubocop/callbacksystems/project_index/confinement_cache.rb +40 -0
- data/lib/rubocop/callbacksystems/project_index/constant_references.rb +74 -0
- data/lib/rubocop/callbacksystems/project_index/corrections.rb +35 -0
- data/lib/rubocop/callbacksystems/project_index/coverage.rb +56 -0
- data/lib/rubocop/callbacksystems/project_index/diagnostics.rb +60 -0
- data/lib/rubocop/callbacksystems/project_index/freshness.rb +104 -0
- data/lib/rubocop/callbacksystems/project_index/loader.rb +9 -0
- data/lib/rubocop/callbacksystems/project_index/location.rb +29 -0
- data/lib/rubocop/callbacksystems/project_index/nested_class_confinement.rb +297 -0
- data/lib/rubocop/callbacksystems/project_index/nested_class_method_dispatch.rb +50 -0
- data/lib/rubocop/callbacksystems/project_index/reading.rb +129 -0
- data/lib/rubocop/callbacksystems/project_index/source_file.rb +59 -0
- data/lib/rubocop/callbacksystems/project_index/source_file_match.rb +57 -0
- data/lib/rubocop/callbacksystems/project_index/support.rb +46 -0
- data/lib/rubocop/callbacksystems/source/comment_blocks.rb +43 -0
- data/lib/rubocop/callbacksystems/source/comments.rb +141 -0
- data/lib/rubocop/callbacksystems/source/file_ast.rb +15 -0
- data/lib/rubocop/callbacksystems/source/files_checksum.rb +43 -0
- data/lib/rubocop/callbacksystems/source/single_line_call.rb +181 -0
- data/lib/rubocop/callbacksystems/source/statement_with_comments.rb +131 -0
- data/lib/rubocop/callbacksystems/testing/cop_helpers.rb +42 -0
- data/lib/rubocop/callbacksystems/testing/fixture_call.rb +30 -0
- data/lib/rubocop/callbacksystems/testing/fixture_names.rb +62 -0
- data/lib/rubocop/callbacksystems/testing/path_mapping.rb +104 -0
- data/lib/rubocop/cop/callbacksystems/application_controller_method_definition.rb +44 -0
- data/lib/rubocop/cop/callbacksystems/assert_with_expected_argument.rb +60 -0
- data/lib/rubocop/cop/callbacksystems/base.rb +134 -0
- data/lib/rubocop/cop/callbacksystems/body_order.rb +478 -0
- data/lib/rubocop/cop/callbacksystems/builder_method_parentheses.rb +74 -0
- data/lib/rubocop/cop/callbacksystems/collapse_multiline_expression.rb +140 -0
- data/lib/rubocop/cop/callbacksystems/comment_block_length.rb +137 -0
- data/lib/rubocop/cop/callbacksystems/complex_conditional.rb +75 -0
- data/lib/rubocop/cop/callbacksystems/controller_test_response_assertion.rb +105 -0
- data/lib/rubocop/cop/callbacksystems/controller_test_response_first_assertion.rb +123 -0
- data/lib/rubocop/cop/callbacksystems/data_clump.rb +339 -0
- data/lib/rubocop/cop/callbacksystems/declarative_method_naming.rb +350 -0
- data/lib/rubocop/cop/callbacksystems/delegate_requires_private_option.rb +81 -0
- data/lib/rubocop/cop/callbacksystems/eager_loading_scope_naming.rb +386 -0
- data/lib/rubocop/cop/callbacksystems/early_return.rb +215 -0
- data/lib/rubocop/cop/callbacksystems/empty_line_before_method.rb +168 -0
- data/lib/rubocop/cop/callbacksystems/empty_line_between_sections.rb +169 -0
- data/lib/rubocop/cop/callbacksystems/empty_private_section.rb +107 -0
- data/lib/rubocop/cop/callbacksystems/expanded_empty_class_or_module.rb +71 -0
- data/lib/rubocop/cop/callbacksystems/inline_memoized_computation.rb +110 -0
- data/lib/rubocop/cop/callbacksystems/job_enqueue_methods.rb +93 -0
- data/lib/rubocop/cop/callbacksystems/max_safe_navigation_depth.rb +118 -0
- data/lib/rubocop/cop/callbacksystems/method_invocation_order.rb +261 -0
- data/lib/rubocop/cop/callbacksystems/nested_class_order.rb +255 -0
- data/lib/rubocop/cop/callbacksystems/no_abbreviations.rb +197 -0
- data/lib/rubocop/cop/callbacksystems/no_anemic_delegation.rb +101 -0
- data/lib/rubocop/cop/callbacksystems/no_anemic_record.rb +309 -0
- data/lib/rubocop/cop/callbacksystems/no_assert_select.rb +31 -0
- data/lib/rubocop/cop/callbacksystems/no_bang_method_without_counterpart.rb +451 -0
- data/lib/rubocop/cop/callbacksystems/no_call_method.rb +73 -0
- data/lib/rubocop/cop/callbacksystems/no_comments_in_test_class_body.rb +159 -0
- data/lib/rubocop/cop/callbacksystems/no_consecutive_appends.rb +261 -0
- data/lib/rubocop/cop/callbacksystems/no_custom_route_actions.rb +124 -0
- data/lib/rubocop/cop/callbacksystems/no_direct_eager_loading.rb +90 -0
- data/lib/rubocop/cop/callbacksystems/no_each_with_early_return.rb +58 -0
- data/lib/rubocop/cop/callbacksystems/no_explicit_public_modifier.rb +81 -0
- data/lib/rubocop/cop/callbacksystems/no_hash_with_default_block.rb +68 -0
- data/lib/rubocop/cop/callbacksystems/no_local_variable_return.rb +173 -0
- data/lib/rubocop/cop/callbacksystems/no_manual_accumulation.rb +188 -0
- data/lib/rubocop/cop/callbacksystems/no_mixed_memoization.rb +145 -0
- data/lib/rubocop/cop/callbacksystems/no_nested_modules_in_classes.rb +78 -0
- data/lib/rubocop/cop/callbacksystems/no_nil_at_end_of_method.rb +74 -0
- data/lib/rubocop/cop/callbacksystems/no_redundant_validation_name.rb +37 -0
- data/lib/rubocop/cop/callbacksystems/no_redundant_wrapper_method.rb +161 -0
- data/lib/rubocop/cop/callbacksystems/no_section_divider_comments.rb +105 -0
- data/lib/rubocop/cop/callbacksystems/no_send_in_tests.rb +75 -0
- data/lib/rubocop/cop/callbacksystems/no_service_objects.rb +64 -0
- data/lib/rubocop/cop/callbacksystems/no_single_letter_names.rb +47 -0
- data/lib/rubocop/cop/callbacksystems/no_singleton_class_in_modules.rb +43 -0
- data/lib/rubocop/cop/callbacksystems/no_tap_collection_building.rb +173 -0
- data/lib/rubocop/cop/callbacksystems/no_turbo_stream_in_controllers.rb +52 -0
- data/lib/rubocop/cop/callbacksystems/no_typographic_clutter.rb +159 -0
- data/lib/rubocop/cop/callbacksystems/no_writes_in_get_actions.rb +149 -0
- data/lib/rubocop/cop/callbacksystems/ordered_mixin_arguments.rb +78 -0
- data/lib/rubocop/cop/callbacksystems/plural_controller_names.rb +103 -0
- data/lib/rubocop/cop/callbacksystems/prefer_attr_reader_over_instance_variable.rb +86 -0
- data/lib/rubocop/cop/callbacksystems/prefer_backslash_continuation.rb +478 -0
- data/lib/rubocop/cop/callbacksystems/prefer_class_for_state.rb +532 -0
- data/lib/rubocop/cop/callbacksystems/prefer_class_new_for_empty_nested_class.rb +247 -0
- data/lib/rubocop/cop/callbacksystems/prefer_class_over_builder_block.rb +124 -0
- data/lib/rubocop/cop/callbacksystems/prefer_default_argument.rb +225 -0
- data/lib/rubocop/cop/callbacksystems/prefer_delegate.rb +242 -0
- data/lib/rubocop/cop/callbacksystems/prefer_delegate_over_instance_variable_assignment.rb +175 -0
- data/lib/rubocop/cop/callbacksystems/prefer_excluding.rb +204 -0
- data/lib/rubocop/cop/callbacksystems/prefer_extract_associated.rb +54 -0
- data/lib/rubocop/cop/callbacksystems/prefer_it_block_parameter.rb +156 -0
- data/lib/rubocop/cop/callbacksystems/prefer_many.rb +57 -0
- data/lib/rubocop/cop/callbacksystems/prefer_method_over_lambda.rb +211 -0
- data/lib/rubocop/cop/callbacksystems/prefer_method_over_variable.rb +172 -0
- data/lib/rubocop/cop/callbacksystems/prefer_module_for_static_class.rb +286 -0
- data/lib/rubocop/cop/callbacksystems/prefer_ordinal_array_access.rb +136 -0
- data/lib/rubocop/cop/callbacksystems/prefer_positive_wrap.rb +265 -0
- data/lib/rubocop/cop/callbacksystems/prefer_squish.rb +47 -0
- data/lib/rubocop/cop/callbacksystems/prefer_stringify_keys.rb +24 -0
- data/lib/rubocop/cop/callbacksystems/prefer_symbolize_keys.rb +24 -0
- data/lib/rubocop/cop/callbacksystems/prefer_turbo_page_refresh_broadcasts.rb +40 -0
- data/lib/rubocop/cop/callbacksystems/private_constants_in_private_section.rb +160 -0
- data/lib/rubocop/cop/callbacksystems/private_delegate_placement.rb +342 -0
- data/lib/rubocop/cop/callbacksystems/private_methods_in_private_section.rb +70 -0
- data/lib/rubocop/cop/callbacksystems/private_nested_class_method_visibility.rb +269 -0
- data/lib/rubocop/cop/callbacksystems/private_nested_classes.rb +94 -0
- data/lib/rubocop/cop/callbacksystems/public_methods_must_have_tests.rb +190 -0
- data/lib/rubocop/cop/callbacksystems/redundant_concern_extend.rb +157 -0
- data/lib/rubocop/cop/callbacksystems/redundant_constant_freeze.rb +57 -0
- data/lib/rubocop/cop/callbacksystems/redundant_constant_namespace.rb +134 -0
- data/lib/rubocop/cop/callbacksystems/reflow_comments.rb +133 -0
- data/lib/rubocop/cop/callbacksystems/repeated_fixture_in_tests.rb +106 -0
- data/lib/rubocop/cop/callbacksystems/restricted_controller_actions.rb +85 -0
- data/lib/rubocop/cop/callbacksystems/routes_module_scope.rb +336 -0
- data/lib/rubocop/cop/callbacksystems/single_line_setup_block.rb +107 -0
- data/lib/rubocop/cop/callbacksystems/single_singleton_class.rb +49 -0
- data/lib/rubocop/cop/callbacksystems/single_top_level_class_or_module.rb +160 -0
- data/lib/rubocop/cop/callbacksystems/single_use_fixture_variable.rb +211 -0
- data/lib/rubocop/cop/callbacksystems/single_use_setup_variable.rb +684 -0
- data/lib/rubocop/cop/callbacksystems/singular_scope_names.rb +47 -0
- data/lib/rubocop/cop/callbacksystems/space_inside_percent_literal.rb +73 -0
- data/lib/rubocop/cop/callbacksystems/system_test_browser_driver.rb +30 -0
- data/lib/rubocop/cop/callbacksystems/test_method_definition.rb +86 -0
- data/lib/rubocop/cop/callbacksystems/test_method_order.rb +319 -0
- data/lib/rubocop/cop/callbacksystems/too_many_instance_variables.rb +78 -0
- data/lib/rubocop/cop/callbacksystems/too_many_local_variables.rb +77 -0
- data/lib/rubocop/cop/callbacksystems/too_many_validation_guards.rb +84 -0
- data/lib/rubocop/cop/callbacksystems/unnecessary_local_variable.rb +176 -0
- data/lib/rubocop/cop/callbacksystems/unused_private_method_in_nested_class.rb +203 -0
- data/lib/rubocop-callbacksystems.rb +21 -0
- data/rubocop.yml +685 -0
- 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
|