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,339 @@
|
|
|
1
|
+
# Detects a set of parameter names that travel together across a class's or
|
|
2
|
+
# module's private methods, suggesting they belong to an object of their own,
|
|
3
|
+
# usually a private nested class holding them as instance state. Only private
|
|
4
|
+
# methods are examined, since a public method's signature answers to an
|
|
5
|
+
# interface, not internal threading, which is also why the JS rule no-data-clump
|
|
6
|
+
# skips exported functions and public members. Methods under `class << self`
|
|
7
|
+
# join the group of the class that holds them.
|
|
8
|
+
#
|
|
9
|
+
# The set is found as a connected component in the co-occurrence graph of shared
|
|
10
|
+
# parameter names, meaning names used by two or more methods, linked whenever they
|
|
11
|
+
# appear together in one method's signature. Because membership follows the
|
|
12
|
+
# graph, not an exact repeated tuple, this catches names sharing *varying*
|
|
13
|
+
# combinations, as in `m1(a, b)`, `m2(a, c)`, `m3(b, c)`, where no single pair
|
|
14
|
+
# repeats. Only methods carrying two or more of the names count toward a set,
|
|
15
|
+
# since one name is a value arriving, not a concept being passed, and counting
|
|
16
|
+
# such bystanders would let a lone name clear the set threshold instead of its
|
|
17
|
+
# own higher one.
|
|
18
|
+
# A one-letter name is left out, since it is a placeholder `NoSingleLetterNames` already flags.
|
|
19
|
+
#
|
|
20
|
+
# How far a set has to spread depends on how much its shape already tells. A
|
|
21
|
+
# signature repeated verbatim counts from `MinMethodsForRepeatedSignature`
|
|
22
|
+
# methods, two by default, because nothing in those parameter lists explains the
|
|
23
|
+
# co-occurrence except the concept itself. Names that come with extras of their
|
|
24
|
+
# own might merely have met, so those need `MinMethods`, three by default, and a
|
|
25
|
+
# lone name threaded through the class has to reach `MinMethodsForSingleParam`,
|
|
26
|
+
# six by default. The recursion subject is exempt, since a name passed both as
|
|
27
|
+
# itself and as a derivative of itself in the same call (`walk(node.child, node)`)
|
|
28
|
+
# changes at every step and cannot become shared state.
|
|
29
|
+
#
|
|
30
|
+
# @example
|
|
31
|
+
# # bad - a signature repeated verbatim is telling from two methods already
|
|
32
|
+
# class Order
|
|
33
|
+
# private
|
|
34
|
+
# def validate(user, account); end
|
|
35
|
+
# def execute(user, account); end
|
|
36
|
+
# end
|
|
37
|
+
#
|
|
38
|
+
# # bad - a shared core with extras of its own, once it reaches three methods
|
|
39
|
+
# class Order
|
|
40
|
+
# private
|
|
41
|
+
# def create(name, email, phone); end
|
|
42
|
+
# def validate(name, email, address); end
|
|
43
|
+
# def save(name, phone, address); end
|
|
44
|
+
# end
|
|
45
|
+
#
|
|
46
|
+
# # bad - one value threaded through six methods
|
|
47
|
+
# class Visitor
|
|
48
|
+
# private
|
|
49
|
+
# def first(node); end
|
|
50
|
+
# def second(node); end
|
|
51
|
+
# def third(node); end
|
|
52
|
+
# def fourth(node); end
|
|
53
|
+
# def fifth(node); end
|
|
54
|
+
# def sixth(node); end
|
|
55
|
+
# end
|
|
56
|
+
#
|
|
57
|
+
# # good - a private nested class holds the values as state
|
|
58
|
+
# class Order
|
|
59
|
+
# def process
|
|
60
|
+
# context = Context.new(user, account)
|
|
61
|
+
# context.validate
|
|
62
|
+
# context.execute
|
|
63
|
+
# end
|
|
64
|
+
#
|
|
65
|
+
# private
|
|
66
|
+
# class Context
|
|
67
|
+
# def initialize(user, account)
|
|
68
|
+
# @user = user
|
|
69
|
+
# @account = account
|
|
70
|
+
# end
|
|
71
|
+
#
|
|
72
|
+
# def validate; end
|
|
73
|
+
# def execute; end
|
|
74
|
+
# end
|
|
75
|
+
# end
|
|
76
|
+
#
|
|
77
|
+
class RuboCop::Cop::Callbacksystems::DataClump < RuboCop::Cop::Callbacksystems::Base
|
|
78
|
+
def on_class(node)
|
|
79
|
+
report_each ParameterClumps.new(node, limits)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
alias on_module on_class
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
def limits
|
|
86
|
+
Limits.new \
|
|
87
|
+
min_methods: cop_config["MinMethods"],
|
|
88
|
+
min_methods_for_repeated_signature: cop_config["MinMethodsForRepeatedSignature"],
|
|
89
|
+
min_methods_for_single_parameter: cop_config["MinMethodsForSingleParam"]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
class ParameterClumps
|
|
93
|
+
include RuboCop::Callbacksystems::Helpers
|
|
94
|
+
|
|
95
|
+
def initialize(node, limits)
|
|
96
|
+
@node = node
|
|
97
|
+
@limits = limits
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def each_offense
|
|
101
|
+
clumps.each { yield RuboCop::Callbacksystems::Offense.new(it.location, it.message) }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
attr_reader :node, :limits
|
|
106
|
+
|
|
107
|
+
def clumps
|
|
108
|
+
SignatureGroup.new(signatures, limits).clumps
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def signatures
|
|
112
|
+
internal_methods.map { Signature.new(it, exempt_names) }.select { it.names.any? }
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def internal_methods
|
|
116
|
+
@internal_methods ||= direct_method_nodes_in(node.body).select { internal_method?(it) }
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def internal_method?(method_node)
|
|
120
|
+
private_nested_class?(node) || private_method?(method_node)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def exempt_names
|
|
124
|
+
@exempt_names ||= internal_methods.flat_map { recursion_subject_names_of(it) }.to_set
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def recursion_subject_names_of(method_node)
|
|
128
|
+
RuboCop::Callbacksystems::Execution::Immediate.new(method_node.body).nodes_of_type(:send, :csend)
|
|
129
|
+
.flat_map { recursion_subjects_in(it) }
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Reads a method group's shared names as connected components, each its own hidden object.
|
|
134
|
+
class SignatureGroup
|
|
135
|
+
MIN_SHARING_METHODS = 2
|
|
136
|
+
|
|
137
|
+
def initialize(signatures, limits)
|
|
138
|
+
@signatures = signatures
|
|
139
|
+
@limits = limits
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def clumps
|
|
143
|
+
candidates.select { limits.reached_by?(it) }
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
private
|
|
147
|
+
attr_reader :signatures, :limits
|
|
148
|
+
|
|
149
|
+
def candidates
|
|
150
|
+
graph.map { clump_for(it) }
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def graph
|
|
154
|
+
Graph.new(shared_names, signatures)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def shared_names
|
|
158
|
+
name_counts.select { |_, count| count >= MIN_SHARING_METHODS }.keys
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def name_counts
|
|
162
|
+
signatures.flat_map(&:names).tally
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def clump_for(names)
|
|
166
|
+
Clump.new(signatures.select { it.counts_toward?(names) }, names)
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# The shared names linked by the signatures they appear in together, read as its connected components.
|
|
171
|
+
class Graph
|
|
172
|
+
include Enumerable
|
|
173
|
+
|
|
174
|
+
delegate :each, to: :components
|
|
175
|
+
|
|
176
|
+
def initialize(shared_names, signatures)
|
|
177
|
+
@shared_names = shared_names
|
|
178
|
+
@signatures = signatures
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
private
|
|
182
|
+
attr_reader :shared_names, :signatures
|
|
183
|
+
|
|
184
|
+
def components
|
|
185
|
+
each_component.to_a
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def each_component
|
|
189
|
+
if block_given?
|
|
190
|
+
remaining = shared_names.to_set
|
|
191
|
+
until remaining.empty?
|
|
192
|
+
component = component_of(remaining.first)
|
|
193
|
+
yield component
|
|
194
|
+
remaining.subtract(component)
|
|
195
|
+
end
|
|
196
|
+
else
|
|
197
|
+
enum_for(__method__)
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def component_of(name)
|
|
202
|
+
closure_of(Set[name])
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def closure_of(names)
|
|
206
|
+
names.dup.tap do |reached|
|
|
207
|
+
pending = names.to_a
|
|
208
|
+
position = 0
|
|
209
|
+
visited_signatures = {}.compare_by_identity
|
|
210
|
+
|
|
211
|
+
until position == pending.size
|
|
212
|
+
signatures_by_name.fetch(pending[position]).each do |signature|
|
|
213
|
+
next if visited_signatures.key?(signature)
|
|
214
|
+
|
|
215
|
+
visited_signatures[signature] = true
|
|
216
|
+
signature.names.each do |neighbor|
|
|
217
|
+
pending << neighbor if shared_name?(neighbor) && reached.add?(neighbor)
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
position += 1
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def signatures_by_name
|
|
226
|
+
@signatures_by_name ||= shared_names.index_with { [] }.tap do |index|
|
|
227
|
+
signatures.each do |signature|
|
|
228
|
+
signature.names.each { index.fetch(it) << signature if index.key?(it) }
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def shared_name?(name)
|
|
234
|
+
shared_name_index.include?(name)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def shared_name_index
|
|
238
|
+
@shared_name_index ||= shared_names.to_set
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
class Clump < Data.define(:signatures, :names)
|
|
243
|
+
MESSAGE = "Parameters `%<parameters>s` travel together through %<count>d private methods (`%<methods>s`). " \
|
|
244
|
+
"Consider a private nested class holding them as instance state."
|
|
245
|
+
SINGLE_PARAMETER_MESSAGE = "Parameter `%<parameter>s` is threaded through %<count>d private methods " \
|
|
246
|
+
"(`%<methods>s`). Consider a private nested class holding it as instance state."
|
|
247
|
+
|
|
248
|
+
def message
|
|
249
|
+
lone_name? ? threaded_parameter_message : shared_parameters_message
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def lone_name?
|
|
253
|
+
names.one?
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Stronger than a shared core, since the methods take the set and nothing besides.
|
|
257
|
+
def repeated_signature?
|
|
258
|
+
signatures.map(&:sorted_names).uniq.one?
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def location
|
|
262
|
+
signatures.first.node.loc.name
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def count
|
|
266
|
+
signatures.size
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
private
|
|
270
|
+
def threaded_parameter_message
|
|
271
|
+
format(SINGLE_PARAMETER_MESSAGE, parameter: sorted_parameter_names.first, count:, methods: method_names)
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def sorted_parameter_names
|
|
275
|
+
names.sort
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def method_names
|
|
279
|
+
signatures.map(&:method_name).join(", ")
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def shared_parameters_message
|
|
283
|
+
format(MESSAGE, parameters: sorted_parameter_names.join(", "), count:, methods: method_names)
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
class Signature
|
|
288
|
+
include RuboCop::Callbacksystems::Helpers
|
|
289
|
+
|
|
290
|
+
attr_reader :node, :names
|
|
291
|
+
|
|
292
|
+
delegate :method_name, to: :node
|
|
293
|
+
|
|
294
|
+
def initialize(node, exempt_names)
|
|
295
|
+
@node = node
|
|
296
|
+
@names = parameter_names_of(node).uniq.reject { exempt_names.include?(it) || it.length == 1 }
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# A method carrying one name of a set is a bystander, so a set needs two of its names to appear in a signature.
|
|
300
|
+
def counts_toward?(wanted)
|
|
301
|
+
shared_count_in(wanted) >= (wanted.many? ? 2 : 1)
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def shared_count_in(wanted)
|
|
305
|
+
names.count { wanted.include?(it) }
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# A one-letter name is a placeholder rather than a concept, and `NoSingleLetterNames` already asks for a word.
|
|
309
|
+
def sorted_names
|
|
310
|
+
@sorted_names ||= names.sort
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# The thresholds mirror the JS rule no-data-clump, so the two configs agree on what qualifies.
|
|
315
|
+
class Limits
|
|
316
|
+
def initialize(min_methods:, min_methods_for_repeated_signature:, min_methods_for_single_parameter:)
|
|
317
|
+
@min_methods = min_methods
|
|
318
|
+
@min_methods_for_repeated_signature = min_methods_for_repeated_signature
|
|
319
|
+
@min_methods_for_single_parameter = min_methods_for_single_parameter
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def reached_by?(clump)
|
|
323
|
+
clump.count >= reach_for(clump)
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
private
|
|
327
|
+
attr_reader :min_methods, :min_methods_for_repeated_signature, :min_methods_for_single_parameter
|
|
328
|
+
|
|
329
|
+
def reach_for(clump)
|
|
330
|
+
if clump.lone_name?
|
|
331
|
+
min_methods_for_single_parameter
|
|
332
|
+
elsif clump.repeated_signature?
|
|
333
|
+
min_methods_for_repeated_signature
|
|
334
|
+
else
|
|
335
|
+
min_methods
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
end
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
# A method that returns a value should be named for the value it hands back
|
|
2
|
+
# (`total`, `user_by_id`), not for the imperative action it performs
|
|
3
|
+
# (`compute_total`, `get_user`). The imperative name reads as a command when the
|
|
4
|
+
# thing is really a query. We flag a method whose name leads with a producer verb
|
|
5
|
+
# and carries a noun we can rename it to (`compute_total` -> `total`). A bare verb
|
|
6
|
+
# (`fetch`) is left alone: with no noun there is nothing to rename to. A verb
|
|
7
|
+
# followed straight by a connector (`find_in_scope`, `load_for`) is left alone
|
|
8
|
+
# too: the phrase that remains (`in_scope`, `for`) names no value on its own, and
|
|
9
|
+
# the leading word often reads as a noun in context (`load_for person` is "the
|
|
10
|
+
# load for a person"), so the rename is too contextual to pick. With arguments and
|
|
11
|
+
# no connector in the name we only flag it (`get_user(id)`): a bare noun beside an
|
|
12
|
+
# argument (`user(id)`) relates nothing, and the relator is the author's call.
|
|
13
|
+
#
|
|
14
|
+
# The verb list is curated, not taken from a lexicon: `get`/`find`/`build` are not
|
|
15
|
+
# dropped from any dictionary, but are bad as method prefixes by code convention.
|
|
16
|
+
# Excluded on purpose: noun-verbs that name a value fine (`count`, `name`,
|
|
17
|
+
# `value`, `order`, `state`...) and command verbs that act rather than produce
|
|
18
|
+
# (`save`, `render`, `update`, `send`, `handle`, `process`...).
|
|
19
|
+
#
|
|
20
|
+
# We flag a producer-verb method only when it just *delivers* a value: it
|
|
21
|
+
# changes no state, calls no mutator, does no I/O. A method that *does* something
|
|
22
|
+
# earns its verb and is left alone, even named with a producer verb: `create_user`
|
|
23
|
+
# that calls `save!`, an association `build` that mutates the collection.
|
|
24
|
+
# Construction is delivery, not doing: `Foo.new(args)` only hands a value back, so
|
|
25
|
+
# a pure factory is flagged. Building a collaborator and then asking it for
|
|
26
|
+
# something is a third case: what happens inside is not readable here, so
|
|
27
|
+
# `Commenter.new(card).comment` leaves the name alone.
|
|
28
|
+
#
|
|
29
|
+
# Two more cases are left alone, both signs the name is intentional rather than
|
|
30
|
+
# careless. A method that delegates within its own verb family (`format_money`
|
|
31
|
+
# calling `format_short`, `find_account_by_cookie` calling `find_by`) mirrors a
|
|
32
|
+
# real operation of the same name, so renaming it would break the family. And a
|
|
33
|
+
# method whose suggested noun already names a sibling (`build_prices` beside
|
|
34
|
+
# `attr_reader :prices`, `lookup_key` beside `key`) is the separate-builder
|
|
35
|
+
# pattern: the value already has its declarative name, this is its helper.
|
|
36
|
+
#
|
|
37
|
+
# Ruby divergence from the JS rule: JS can gate on "returns a value" because a
|
|
38
|
+
# function can fall off the end returning `undefined`. In Ruby every method
|
|
39
|
+
# returns its last expression, so "delivers" is the *absence of effects*, not the
|
|
40
|
+
# presence of a return. Predicates (`valid?`), bang methods (`save!`), setters
|
|
41
|
+
# (`foo=`), operators, and `initialize` stay exempt by Ruby convention.
|
|
42
|
+
#
|
|
43
|
+
# @example
|
|
44
|
+
# # bad - imperative name for a value (no arguments): name it for the value
|
|
45
|
+
# def compute_total
|
|
46
|
+
# line_items.sum(&:price)
|
|
47
|
+
# end
|
|
48
|
+
#
|
|
49
|
+
# # bad - imperative name for a value built from an argument
|
|
50
|
+
# def get_user(id)
|
|
51
|
+
# users[id]
|
|
52
|
+
# end
|
|
53
|
+
#
|
|
54
|
+
# # good - the bare verb is allowed: there is no noun to rename it to
|
|
55
|
+
# def compute
|
|
56
|
+
# heavy_work
|
|
57
|
+
# end
|
|
58
|
+
#
|
|
59
|
+
# # good - verb followed straight by a connector names no value to rename to
|
|
60
|
+
# def load_for(person)
|
|
61
|
+
# events_for(person) + blocks_for(person)
|
|
62
|
+
# end
|
|
63
|
+
#
|
|
64
|
+
# # good - delegates within its own verb family: it mirrors a real operation
|
|
65
|
+
# def format_money(money)
|
|
66
|
+
# Worldwide.currency(money).format_short(money)
|
|
67
|
+
# end
|
|
68
|
+
#
|
|
69
|
+
# # good - the suggested noun already names a sibling (`attr_reader :prices`)
|
|
70
|
+
# def build_prices
|
|
71
|
+
# properties.to_h { |currency, cents| [ currency, Money.new(cents) ] }
|
|
72
|
+
# end
|
|
73
|
+
#
|
|
74
|
+
# # good - named for the value it returns
|
|
75
|
+
# def total
|
|
76
|
+
# line_items.sum(&:price)
|
|
77
|
+
# end
|
|
78
|
+
#
|
|
79
|
+
# # good - noun-verb reads as a noun
|
|
80
|
+
# def count
|
|
81
|
+
# items.length
|
|
82
|
+
# end
|
|
83
|
+
#
|
|
84
|
+
# # good - predicates, bang methods, and setters are exempt by convention
|
|
85
|
+
# def valid?
|
|
86
|
+
# errors.empty?
|
|
87
|
+
# end
|
|
88
|
+
#
|
|
89
|
+
class RuboCop::Cop::Callbacksystems::DeclarativeMethodNaming < RuboCop::Cop::Callbacksystems::Base
|
|
90
|
+
def on_new_investigation
|
|
91
|
+
@declared_values = DeclaredValues.new
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def on_def(node)
|
|
95
|
+
report MethodName.new(node, declared_values:)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
alias on_defs on_def
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
PRODUCER_VERBS = %w[
|
|
102
|
+
get fetch retrieve obtain acquire load
|
|
103
|
+
compute calculate derive determine evaluate deduce infer
|
|
104
|
+
generate produce fabricate assemble construct build make create
|
|
105
|
+
convert transform translate cast coerce
|
|
106
|
+
serialize deserialize stringify normalize denormalize parse extract
|
|
107
|
+
format summarize aggregate tabulate
|
|
108
|
+
find lookup locate search select pick choose
|
|
109
|
+
collect gather accumulate compile
|
|
110
|
+
resolve
|
|
111
|
+
].to_set
|
|
112
|
+
|
|
113
|
+
CONNECTORS = %w[
|
|
114
|
+
for of from at in on by with within without
|
|
115
|
+
into onto over under above below beneath behind
|
|
116
|
+
beside before after between across through throughout
|
|
117
|
+
toward towards alongside during via to per until
|
|
118
|
+
since against among around near beyond
|
|
119
|
+
].to_set
|
|
120
|
+
|
|
121
|
+
EFFECTS = %i[
|
|
122
|
+
save update create build destroy delete insert
|
|
123
|
+
insert_all upsert upsert_all update_all delete_all destroy_all
|
|
124
|
+
push pop shift unshift prepend concat append << store replace add clear
|
|
125
|
+
each each_with_index puts print pp write
|
|
126
|
+
touch mkdir mkdir_p mkpath makedirs rm rm_f rm_rf rmdir remove_entry remove_dir
|
|
127
|
+
cp cp_r mv ln_s chmod chown
|
|
128
|
+
].to_set
|
|
129
|
+
|
|
130
|
+
READER_MACROS = %i[ attr_reader attr_accessor attr_writer ].to_set
|
|
131
|
+
SIDE_EFFECT_NODE_TYPES = %i[ casgn cvasgn gvasgn ivasgn super xstr yield zsuper ]
|
|
132
|
+
NETWORK_METHODS = %i[ get post put patch delete request start ].to_set
|
|
133
|
+
FILESYSTEM_RECEIVERS = %w[ File IO ].to_set
|
|
134
|
+
FILESYSTEM_METHODS = %i[ binread foreach open read readlines sysopen ].to_set
|
|
135
|
+
|
|
136
|
+
attr_reader :declared_values
|
|
137
|
+
|
|
138
|
+
# Value names indexed once for each body whose methods ask whether their suggested noun is already taken.
|
|
139
|
+
class DeclaredValues
|
|
140
|
+
include RuboCop::Callbacksystems::Helpers
|
|
141
|
+
|
|
142
|
+
def initialize
|
|
143
|
+
@sibling_methods = RuboCop::Callbacksystems::Methods::Siblings.new
|
|
144
|
+
@reader_names_by_container = {}.compare_by_identity
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def include?(name, beside:)
|
|
148
|
+
sibling_methods.named(name.to_sym, beside:).any? || reader_names_beside(beside).include?(name)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
private
|
|
152
|
+
attr_reader :sibling_methods, :reader_names_by_container
|
|
153
|
+
|
|
154
|
+
def reader_names_beside(method)
|
|
155
|
+
domain = RuboCop::Callbacksystems::Methods::Domain.new(method)
|
|
156
|
+
reader_names_in(domain.container).fetch(domain.identity) { Set.new }
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def reader_names_in(container)
|
|
160
|
+
if container
|
|
161
|
+
reader_names_by_container[container] ||=
|
|
162
|
+
ReaderMacros.new(container.body).each_with_object({}) do |macro, names|
|
|
163
|
+
(names[declaration_domain_of(macro)] ||= Set.new)
|
|
164
|
+
.merge(macro.arguments.select(&:sym_type?).map { it.value.to_s })
|
|
165
|
+
end
|
|
166
|
+
else
|
|
167
|
+
{}
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def declaration_domain_of(macro)
|
|
172
|
+
identity = RuboCop::Callbacksystems::Methods::Domain.new(macro).identity
|
|
173
|
+
identity.take(identity.size.pred).freeze
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Reader declarations reached through body sequences, singleton sections, and nested call arguments.
|
|
177
|
+
class ReaderMacros
|
|
178
|
+
include Enumerable
|
|
179
|
+
include RuboCop::Callbacksystems::Helpers
|
|
180
|
+
|
|
181
|
+
def initialize(body)
|
|
182
|
+
@body = body
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def each
|
|
186
|
+
if block_given?
|
|
187
|
+
@pending = [ body ].compact
|
|
188
|
+
until pending.empty?
|
|
189
|
+
advance
|
|
190
|
+
visit { yield it }
|
|
191
|
+
end
|
|
192
|
+
else
|
|
193
|
+
to_enum(__method__)
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
private
|
|
198
|
+
attr_reader :body, :pending, :current
|
|
199
|
+
|
|
200
|
+
def advance
|
|
201
|
+
@current = pending.pop
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def visit
|
|
205
|
+
case current.type
|
|
206
|
+
when :begin, :kwbegin then queue current.each_child_node
|
|
207
|
+
when :sclass then queue [ current.body ].compact
|
|
208
|
+
when :send then visit_send { yield it }
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def queue(nodes)
|
|
213
|
+
pending.concat(nodes.to_a.reverse)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def visit_send
|
|
217
|
+
yield current if reader_macro?
|
|
218
|
+
queue current.arguments
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def reader_macro?
|
|
222
|
+
bare_send?(current) && READER_MACROS.include?(current.method_name)
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
class MethodName
|
|
228
|
+
include RuboCop::Callbacksystems::Helpers
|
|
229
|
+
|
|
230
|
+
IMPERATIVE_NAME = "Rename `%<name>s` to `%<suggestion>s`: name it for the value, not the action `%<verb>s`."
|
|
231
|
+
IMPERATIVE_NAME_RELATE = "Rename `%<name>s` for the value, related to its argument, not the action `%<verb>s`."
|
|
232
|
+
|
|
233
|
+
def initialize(node, declared_values:)
|
|
234
|
+
@node = node
|
|
235
|
+
@declared_values = declared_values
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def offense
|
|
239
|
+
RuboCop::Callbacksystems::Offense.new(node.loc.name, message) if imperatively_named?
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
private
|
|
243
|
+
attr_reader :node, :declared_values
|
|
244
|
+
|
|
245
|
+
def imperatively_named?
|
|
246
|
+
eligible? && imperative_producer? && delivers_only? &&
|
|
247
|
+
!delegates_within_verb_family? && !value_already_named?
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def eligible?
|
|
251
|
+
!node.predicate_method? && !node.bang_method? && !node.assignment_method? &&
|
|
252
|
+
!node.operator_method? && !node.method?(:initialize)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def imperative_producer?
|
|
256
|
+
PRODUCER_VERBS.include?(verb) && !noun.nil?
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def verb
|
|
260
|
+
segments.first
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def segments
|
|
264
|
+
@segments ||= node.method_name.to_s.split("_")
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# A rest led by a connector (`find_in_block`) is a prepositional phrase naming nothing, so it yields no noun.
|
|
268
|
+
def noun
|
|
269
|
+
rest.join("_") if rest.any? && CONNECTORS.exclude?(rest.first)
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def rest
|
|
273
|
+
segments.drop(1)
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def delivers_only?
|
|
277
|
+
RuboCop::Callbacksystems::Execution::Immediate.new(node.body).none? do |descendant|
|
|
278
|
+
descendant.type?(*SIDE_EFFECT_NODE_TYPES, :call) && SideEffect.new(descendant).present?
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def delegates_within_verb_family?
|
|
283
|
+
node.each_descendant(:call).any? { it.method_name.to_s.split("_").first == verb }
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def value_already_named?
|
|
287
|
+
declared_values.include?(noun, beside: node)
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def message
|
|
291
|
+
format(message_template, name: node.method_name, verb: verb, suggestion: noun)
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def message_template
|
|
295
|
+
needs_relator? ? IMPERATIVE_NAME_RELATE : IMPERATIVE_NAME
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def needs_relator?
|
|
299
|
+
arguments? && !carries_connector?
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def arguments?
|
|
303
|
+
node.arguments.any?
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def carries_connector?
|
|
307
|
+
rest.any? { CONNECTORS.include?(it) }
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
class SideEffect
|
|
312
|
+
include RuboCop::Callbacksystems::Helpers
|
|
313
|
+
|
|
314
|
+
def initialize(node)
|
|
315
|
+
@node = node
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def present?
|
|
319
|
+
!node.call_type? || mutating_call?
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
private
|
|
323
|
+
attr_reader :node
|
|
324
|
+
|
|
325
|
+
def mutating_call?
|
|
326
|
+
node.bang_method? || node.setter_method? || EFFECTS.include?(node.method_name) ||
|
|
327
|
+
asks_a_built_collaborator? || performs_io?
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def asks_a_built_collaborator?
|
|
331
|
+
node.receiver&.call_type? && node.receiver.method?(:new)
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def performs_io?
|
|
335
|
+
performs_network_io? || performs_filesystem_io?
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def performs_network_io?
|
|
339
|
+
receiver_constant.to_s.start_with?("Net::HTTP") && NETWORK_METHODS.include?(node.method_name)
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def receiver_constant
|
|
343
|
+
@receiver_constant ||= constant_name_of(node.receiver)&.delete_prefix("::")
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def performs_filesystem_io?
|
|
347
|
+
FILESYSTEM_RECEIVERS.include?(receiver_constant) && FILESYSTEM_METHODS.include?(node.method_name)
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
end
|