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,532 @@
|
|
|
1
|
+
# Detects a single value threaded through many method calls: a parameter or a
|
|
2
|
+
# local passed as an argument to several different receiverless methods, directly
|
|
3
|
+
# or down a pipeline. Such a value is instance state in disguise: it reads
|
|
4
|
+
# better as the state of a class, with the methods that receive it becoming
|
|
5
|
+
# instance methods that no longer need the argument.
|
|
6
|
+
#
|
|
7
|
+
# The value is followed by *data flow*: every receiverless method it is passed to
|
|
8
|
+
# as an argument, then (through that callee's matching parameter) wherever it
|
|
9
|
+
# flows next, transitively across the class. So a value handed from `rewrite` to
|
|
10
|
+
# `process` to `decorate` counts all three, even though no single method passes it
|
|
11
|
+
# four times. The flow is not followed through a *recursive* method: there the
|
|
12
|
+
# value is a moving cursor over a structure, not one value held as state.
|
|
13
|
+
#
|
|
14
|
+
# This complements DataClump, which catches *groups* of arguments shared across
|
|
15
|
+
# method *signatures*. Here a *single* value threaded around is the smell.
|
|
16
|
+
#
|
|
17
|
+
# @example
|
|
18
|
+
# # bad - `node` is passed to every helper
|
|
19
|
+
# def rewrite(node)
|
|
20
|
+
# validate(node)
|
|
21
|
+
# decorate(node)
|
|
22
|
+
# persist(node)
|
|
23
|
+
# index(node)
|
|
24
|
+
# end
|
|
25
|
+
#
|
|
26
|
+
# # bad - `node` is threaded down a pipeline
|
|
27
|
+
# def rewrite(node)
|
|
28
|
+
# process(node)
|
|
29
|
+
# end
|
|
30
|
+
#
|
|
31
|
+
# def process(value)
|
|
32
|
+
# decorate(value)
|
|
33
|
+
# persist(value)
|
|
34
|
+
# index(value)
|
|
35
|
+
# end
|
|
36
|
+
#
|
|
37
|
+
# # good - `node` becomes the state of a class; the helpers become its methods
|
|
38
|
+
# def rewrite(node)
|
|
39
|
+
# Rewrite.new(node).perform
|
|
40
|
+
# end
|
|
41
|
+
#
|
|
42
|
+
# class Rewrite
|
|
43
|
+
# def initialize(node)
|
|
44
|
+
# @node = node
|
|
45
|
+
# end
|
|
46
|
+
#
|
|
47
|
+
# def perform
|
|
48
|
+
# validate
|
|
49
|
+
# decorate
|
|
50
|
+
# persist
|
|
51
|
+
# index
|
|
52
|
+
# end
|
|
53
|
+
# end
|
|
54
|
+
#
|
|
55
|
+
class RuboCop::Cop::Callbacksystems::PreferClassForState < RuboCop::Cop::Callbacksystems::Base
|
|
56
|
+
def on_new_investigation
|
|
57
|
+
@method_indexes = MethodIndexes.new(processed_source.ast)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def on_def(node)
|
|
61
|
+
report_each ThreadedValues.new(node, index: method_indexes.for(node), min_methods: cop_config["MinMethods"])
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
alias on_defs on_def
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
Call = Data.define(:method_name, :position)
|
|
68
|
+
FlowEdge = Data.define(:method_name, :target)
|
|
69
|
+
ReachResult = Data.define(:method_names, :cyclic)
|
|
70
|
+
|
|
71
|
+
attr_reader :method_indexes
|
|
72
|
+
|
|
73
|
+
class ThreadedValues
|
|
74
|
+
include RuboCop::Callbacksystems::Helpers
|
|
75
|
+
|
|
76
|
+
MESSAGE = "`%<name>s` is threaded through %<count>d methods. Make it the state of a class."
|
|
77
|
+
|
|
78
|
+
def initialize(node, index:, min_methods:)
|
|
79
|
+
@node = node
|
|
80
|
+
@index = index
|
|
81
|
+
@min_methods = min_methods
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def each_offense
|
|
85
|
+
threaded.each do |name, count|
|
|
86
|
+
yield RuboCop::Callbacksystems::Offense.new \
|
|
87
|
+
definitions.fetch(name),
|
|
88
|
+
format(MESSAGE, name: name, count: count)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
attr_reader :node, :index, :min_methods
|
|
94
|
+
|
|
95
|
+
def threaded
|
|
96
|
+
return {} if recursive_origin?
|
|
97
|
+
|
|
98
|
+
reach_counts.select { |_, count| count >= min_methods }
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def recursive_origin?
|
|
102
|
+
index.recursive?(node)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def reach_counts
|
|
106
|
+
definitions.keys.to_h { [ it, reach_size_for(it) ] }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def definitions
|
|
110
|
+
@definitions ||= parameter_definitions.merge(local_definitions)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def parameter_definitions
|
|
114
|
+
parameter_nodes_of(node).index_by { it.children.first.to_s }.except(*method_assignments.names)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def method_assignments
|
|
118
|
+
@method_assignments ||= index.assignments_for(node)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def local_definitions
|
|
122
|
+
method_assignments.unique_local_definitions.except(*parameter_names)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def parameter_names
|
|
126
|
+
@parameter_names ||= parameter_nodes_of(node).to_set { it.children.first.to_s }
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def reach_size_for(name)
|
|
130
|
+
index.reach_size(index.flow(name, node))
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# One index per class and method domain keeps receiverless instance calls away from singleton definitions, and lets
|
|
135
|
+
# every origin reuse the same value-flow graph.
|
|
136
|
+
class MethodIndexes
|
|
137
|
+
include RuboCop::Callbacksystems::Helpers
|
|
138
|
+
|
|
139
|
+
def initialize(ast)
|
|
140
|
+
@methods_by_container = grouped_methods_in(ast)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def for(method)
|
|
144
|
+
domain = MethodDomain.new(method)
|
|
145
|
+
if domain.scope
|
|
146
|
+
indexes_for(domain.container)[domain.scope] ||= MethodIndex.new(methods_in(domain))
|
|
147
|
+
else
|
|
148
|
+
MethodIndex.new([])
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
private
|
|
153
|
+
attr_reader :methods_by_container
|
|
154
|
+
|
|
155
|
+
def grouped_methods_in(ast)
|
|
156
|
+
nodes_in(ast, :class, :module).each_with_object({}.compare_by_identity) do |container, groups|
|
|
157
|
+
groups[container] = direct_method_nodes_in(container.body).group_by { MethodDomain.new(it).scope }
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def indexes_for(container)
|
|
162
|
+
container ? indexes_by_container[container] ||= {} : top_level_indexes
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def indexes_by_container
|
|
166
|
+
@indexes_by_container ||= {}.compare_by_identity
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def top_level_indexes
|
|
170
|
+
@top_level_indexes ||= {}
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def methods_in(domain)
|
|
174
|
+
methods_by_container.fetch(domain.container, {}).fetch(domain.scope, [])
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
class MethodDomain
|
|
179
|
+
delegate :container, to: :domain
|
|
180
|
+
|
|
181
|
+
def initialize(node)
|
|
182
|
+
@node = node
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def scope
|
|
186
|
+
domain.identity if resolvable?
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
private
|
|
190
|
+
attr_reader :node
|
|
191
|
+
|
|
192
|
+
def resolvable?
|
|
193
|
+
!node.defs_type? || node.receiver.self_type?
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def domain
|
|
197
|
+
@domain ||= RuboCop::Callbacksystems::Methods::Domain.new(node)
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Resolves calls within one method domain and memoizes the transitive set reached from every parameter.
|
|
202
|
+
class MethodIndex
|
|
203
|
+
def initialize(methods)
|
|
204
|
+
@methods = methods
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def flow(name, method_node)
|
|
208
|
+
flows_for(method_node)[name] ||= ValueFlow.new(name, method_node, self)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def parameter_flow(name, position)
|
|
212
|
+
method = methods_by_name[name]
|
|
213
|
+
Parameter.new(method, position, self).flow unless method.nil? || recursive?(method)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def recursive?(method)
|
|
217
|
+
calls_for(method).body_method_names.include?(method.method_name)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def assignments_for(method)
|
|
221
|
+
assignments_by_method[method] ||= MethodAssignments.new(method)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def threading_calls_for(name, method:)
|
|
225
|
+
calls_for(method).named(name)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def reach_size(flow)
|
|
229
|
+
reachability.result_for(flow).then { it.cyclic ? 0 : it.method_names.size }
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
private
|
|
233
|
+
attr_reader :methods
|
|
234
|
+
|
|
235
|
+
def flows_for(method)
|
|
236
|
+
flows_by_method[method] ||= {}
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def flows_by_method
|
|
240
|
+
@flows_by_method ||= {}.compare_by_identity
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def methods_by_name
|
|
244
|
+
@methods_by_name ||= methods.index_by(&:method_name)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def calls_for(method)
|
|
248
|
+
calls_by_method[method] ||= ThreadingCalls.new(method)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def calls_by_method
|
|
252
|
+
@calls_by_method ||= {}.compare_by_identity
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def assignments_by_method
|
|
256
|
+
@assignments_by_method ||= {}.compare_by_identity
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def reachability
|
|
260
|
+
@reachability ||= Reachability.new(self)
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# One value, a name within a single method body, and the receiverless calls it is passed to.
|
|
265
|
+
class ValueFlow < Data.define(:name, :method_node, :index)
|
|
266
|
+
def threading_calls
|
|
267
|
+
index.threading_calls_for(name, method: method_node)
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Receiverless calls indexed by every unshadowed local value they carry, with one traversal of the method.
|
|
272
|
+
class ThreadingCalls
|
|
273
|
+
include RuboCop::Callbacksystems::Helpers
|
|
274
|
+
|
|
275
|
+
def initialize(method_node)
|
|
276
|
+
@method_node = method_node
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def named(name)
|
|
280
|
+
calls_by_name.fetch(name) { [] }
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def body_method_names
|
|
284
|
+
calls_by_name
|
|
285
|
+
@body_method_names
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
private
|
|
289
|
+
attr_reader :method_node
|
|
290
|
+
|
|
291
|
+
def calls_by_name
|
|
292
|
+
@calls_by_name ||= {}.tap do |calls|
|
|
293
|
+
@body_method_names = Set.new
|
|
294
|
+
method_node.each_descendant(:send).each { add(it, to: calls) if bare_send?(it) }
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def add(send_node, to:)
|
|
299
|
+
@body_method_names << send_node.method_name if inside_body?(send_node)
|
|
300
|
+
indexed_names = Set.new
|
|
301
|
+
position_known = true
|
|
302
|
+
|
|
303
|
+
send_node.arguments.each_with_index do |argument, position|
|
|
304
|
+
name = unshadowed_name_of(argument)
|
|
305
|
+
add_call(send_node, name, position, to:) if new_name?(name, among: indexed_names) && position_known
|
|
306
|
+
position_known &&= !positional_width_ambiguous?(argument)
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def inside_body?(send_node)
|
|
311
|
+
method_node.body&.source_range&.contains?(send_node.source_range)
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def unshadowed_name_of(argument)
|
|
315
|
+
if argument.lvar_type? && !name_rebound_between?(argument.name, node: argument, boundary: method_node)
|
|
316
|
+
argument.name.to_s
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def new_name?(name, among:)
|
|
321
|
+
name && among.add?(name)
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def add_call(send_node, name, position, to:)
|
|
325
|
+
(to[name] ||= []) << Call.new(send_node.method_name, position)
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def positional_width_ambiguous?(argument)
|
|
329
|
+
argument.type?(:splat, :kwsplat, :forwarded_args) || argument.each_descendant(:kwsplat).any?
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
class Parameter < Data.define(:method_node, :position, :index)
|
|
334
|
+
include RuboCop::Callbacksystems::Helpers
|
|
335
|
+
|
|
336
|
+
def flow
|
|
337
|
+
index.flow(name, method_node) if name && index.assignments_for(method_node).exclude?(name)
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def name
|
|
341
|
+
parameter = method_node.arguments.children[position]
|
|
342
|
+
parameter.children.first.to_s if parameter&.arg_type?
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# Effective local assignments in one method, excluding nested local scopes and explicitly shadowed block locals.
|
|
347
|
+
class MethodAssignments
|
|
348
|
+
include RuboCop::Callbacksystems::Helpers
|
|
349
|
+
|
|
350
|
+
delegate :exclude?, to: :names
|
|
351
|
+
|
|
352
|
+
def initialize(method_node)
|
|
353
|
+
@method_node = method_node
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def names
|
|
357
|
+
@names ||= assignments.to_set { assignment_name_of(it) }
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def unique_local_definitions
|
|
361
|
+
assignments.group_by { assignment_name_of(it) }.filter_map do |name, definitions|
|
|
362
|
+
[ name, definitions.first ] if definitions.one? && definitions.first.lvasgn_type?
|
|
363
|
+
end.to_h
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
private
|
|
367
|
+
attr_reader :method_node
|
|
368
|
+
|
|
369
|
+
def assignments
|
|
370
|
+
@assignments ||= AssignmentsWithin.new(method_node.body).reject do |assignment|
|
|
371
|
+
name_rebound_between?(assignment_name_of(assignment), node: assignment, boundary: method_node)
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def assignment_name_of(assignment)
|
|
376
|
+
assignment.children.first.to_s
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
class AssignmentsWithin
|
|
380
|
+
include Enumerable
|
|
381
|
+
include RuboCop::Callbacksystems::Helpers
|
|
382
|
+
|
|
383
|
+
def initialize(root)
|
|
384
|
+
@root = root
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
def each
|
|
388
|
+
if block_given?
|
|
389
|
+
each_assignment { yield it }
|
|
390
|
+
else
|
|
391
|
+
to_enum(__method__)
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
private
|
|
396
|
+
attr_reader :root
|
|
397
|
+
|
|
398
|
+
def each_assignment
|
|
399
|
+
pending = [ root ].compact
|
|
400
|
+
until pending.empty?
|
|
401
|
+
current = pending.pop
|
|
402
|
+
yield current if current.type?(:lvasgn, :match_var)
|
|
403
|
+
children_of(current).to_a.reverse_each { pending << it }
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
def children_of(node)
|
|
408
|
+
lexical_definition?(node) ? immediate_inputs_of(node) : node.each_child_node
|
|
409
|
+
end
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
# The cached graph shared by every root within one method domain.
|
|
414
|
+
class Reachability
|
|
415
|
+
attr_reader :results
|
|
416
|
+
|
|
417
|
+
def initialize(index)
|
|
418
|
+
@index = index
|
|
419
|
+
@results = {}.compare_by_identity
|
|
420
|
+
@edges = {}.compare_by_identity
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
def result_for(flow)
|
|
424
|
+
results[flow] ||= ReachabilityWalk.new(flow, self).result
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
def edges_for(flow)
|
|
428
|
+
edges[flow] ||= flow.threading_calls.map do |call|
|
|
429
|
+
FlowEdge.new(call.method_name, index.parameter_flow(call.method_name, call.position))
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
private
|
|
434
|
+
attr_reader :index, :edges
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
# An iterative walk of the graph rooted at one value flow.
|
|
438
|
+
class ReachabilityWalk
|
|
439
|
+
def initialize(root, reachability)
|
|
440
|
+
@root = root
|
|
441
|
+
@reachability = reachability
|
|
442
|
+
@stack = []
|
|
443
|
+
@visiting = {}.compare_by_identity
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
def result
|
|
447
|
+
start(root)
|
|
448
|
+
advance until stack.empty?
|
|
449
|
+
results.fetch(root)
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
private
|
|
453
|
+
attr_reader :root, :reachability, :stack, :visiting
|
|
454
|
+
|
|
455
|
+
delegate :edges_for, :results, to: :reachability, private: true
|
|
456
|
+
delegate :edge, to: :frame, private: true
|
|
457
|
+
|
|
458
|
+
def start(flow)
|
|
459
|
+
visiting[flow] = true
|
|
460
|
+
stack << FlowFrame.new(flow, edges_for(flow))
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
def advance
|
|
464
|
+
if frame.complete?
|
|
465
|
+
finish
|
|
466
|
+
elsif edge.target.nil?
|
|
467
|
+
frame.absorb
|
|
468
|
+
else
|
|
469
|
+
advance_to_target
|
|
470
|
+
end
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
def frame
|
|
474
|
+
stack.last
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
def finish
|
|
478
|
+
results[frame.flow] = frame.result
|
|
479
|
+
visiting.delete(frame.flow)
|
|
480
|
+
stack.pop
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
def advance_to_target
|
|
484
|
+
if results.key?(edge.target)
|
|
485
|
+
frame.absorb(results.fetch(edge.target))
|
|
486
|
+
elsif visiting.key?(edge.target)
|
|
487
|
+
frame.absorb(cyclic: true)
|
|
488
|
+
else
|
|
489
|
+
start(edge.target)
|
|
490
|
+
end
|
|
491
|
+
end
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
# One explicit stack frame keeps large pipelines off Ruby's call stack.
|
|
495
|
+
class FlowFrame
|
|
496
|
+
attr_reader :flow
|
|
497
|
+
|
|
498
|
+
def initialize(flow, edges)
|
|
499
|
+
@flow = flow
|
|
500
|
+
@edges = edges
|
|
501
|
+
@position = 0
|
|
502
|
+
@method_names = Set.new
|
|
503
|
+
@cyclic = false
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
def complete?
|
|
507
|
+
position >= edges.size
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
def absorb(result = nil, cyclic: false)
|
|
511
|
+
method_names << edge.method_name
|
|
512
|
+
method_names.merge(result.method_names) if result
|
|
513
|
+
remember_cycle(result, explicit: cyclic)
|
|
514
|
+
@position += 1
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
def edge
|
|
518
|
+
edges[position]
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
def result
|
|
522
|
+
ReachResult.new(method_names:, cyclic:)
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
private
|
|
526
|
+
attr_reader :edges, :position, :method_names, :cyclic
|
|
527
|
+
|
|
528
|
+
def remember_cycle(result, explicit:)
|
|
529
|
+
@cyclic = cyclic || explicit || result&.cyclic || false
|
|
530
|
+
end
|
|
531
|
+
end
|
|
532
|
+
end
|