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,684 @@
|
|
|
1
|
+
# Detects an instance variable assigned in `setup` that at most one test reads.
|
|
2
|
+
# A `setup` assignment says the tests share the value, so a reader keeps it in
|
|
3
|
+
# mind for every test, where one read by a single test belongs in that test,
|
|
4
|
+
# and one no test reads is a record the file loads for nothing.
|
|
5
|
+
#
|
|
6
|
+
# @example
|
|
7
|
+
# # bad - @order is only used in one test
|
|
8
|
+
# setup do
|
|
9
|
+
# @order = orders(:one)
|
|
10
|
+
# end
|
|
11
|
+
#
|
|
12
|
+
# test "order is valid" do
|
|
13
|
+
# assert @order.valid?
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# test "something else" do
|
|
17
|
+
# assert true
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# # bad - @order is not used in any test
|
|
21
|
+
# setup do
|
|
22
|
+
# @order = orders(:one)
|
|
23
|
+
# end
|
|
24
|
+
#
|
|
25
|
+
# # good - variable used in multiple tests
|
|
26
|
+
# setup do
|
|
27
|
+
# @order = orders(:one)
|
|
28
|
+
# end
|
|
29
|
+
#
|
|
30
|
+
# test "order is valid" do
|
|
31
|
+
# assert @order.valid?
|
|
32
|
+
# end
|
|
33
|
+
#
|
|
34
|
+
# test "order has items" do
|
|
35
|
+
# assert @order.items.any?
|
|
36
|
+
# end
|
|
37
|
+
#
|
|
38
|
+
class RuboCop::Cop::Callbacksystems::SingleUseSetupVariable < RuboCop::Cop::Callbacksystems::Base
|
|
39
|
+
include RuboCop::Callbacksystems::Testing::CopHelpers
|
|
40
|
+
extend RuboCop::Cop::AutoCorrector
|
|
41
|
+
|
|
42
|
+
def on_new_investigation
|
|
43
|
+
test_domains.each { report_each it } if processed_source.ast
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
def test_domains
|
|
48
|
+
TestDomains.new(processed_source.ast, processed_source:, setups: setup_blocks, tests: test_blocks)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
class TestContext
|
|
52
|
+
attr_reader :tests, :writes, :setup_callbacks, :processed_source
|
|
53
|
+
|
|
54
|
+
def initialize(tests:, reads:, writes:, setup_callbacks:, processed_source:)
|
|
55
|
+
@tests = tests
|
|
56
|
+
@writes = writes
|
|
57
|
+
@setup_callbacks = setup_callbacks
|
|
58
|
+
@processed_source = processed_source
|
|
59
|
+
@variable_usages = InstanceVariableUsages.new(reads, tests:)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def usage_of(variable_name)
|
|
63
|
+
variable_usages.for(variable_name)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
attr_reader :variable_usages
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Each class or concern owns its setup, tests, and reads; names repeated in another container are unrelated.
|
|
71
|
+
class TestDomains
|
|
72
|
+
include Enumerable
|
|
73
|
+
include RuboCop::Callbacksystems::Helpers
|
|
74
|
+
|
|
75
|
+
def initialize(ast, processed_source:, setups:, tests:)
|
|
76
|
+
@ast = ast
|
|
77
|
+
@processed_source = processed_source
|
|
78
|
+
@setups = setups
|
|
79
|
+
@tests = tests
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def each
|
|
83
|
+
setup_groups.each do |scope, grouped_setups|
|
|
84
|
+
context = TestContext.new(tests: test_groups.fetch(scope, []), reads: read_groups.fetch(scope, []),
|
|
85
|
+
writes: write_groups.fetch(scope, []), setup_callbacks: setup_callback_groups.fetch(scope, []),
|
|
86
|
+
processed_source:)
|
|
87
|
+
|
|
88
|
+
yield TestDomain.new(scope, setups: grouped_setups, context:)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
attr_reader :ast, :processed_source, :setups, :tests
|
|
94
|
+
|
|
95
|
+
def setup_groups
|
|
96
|
+
@setup_groups ||= groups_of(setups)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def groups_of(nodes)
|
|
100
|
+
{}.compare_by_identity.tap do |groups|
|
|
101
|
+
nodes.each { (groups[scope_of(it)] ||= []) << it }
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def scope_of(node)
|
|
106
|
+
RuboCop::Callbacksystems::Methods::Domain.new(node).container || ast
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def test_groups
|
|
110
|
+
@test_groups ||= groups_of(tests)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def read_groups
|
|
114
|
+
@read_groups ||= content_groups_of(nodes_in(ast, :ivar))
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def content_groups_of(nodes)
|
|
118
|
+
{}.compare_by_identity.tap do |groups|
|
|
119
|
+
nodes.each do |node|
|
|
120
|
+
content_scopes_of(node).each { (groups[it] ||= []) << node }
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def content_scopes_of(node)
|
|
126
|
+
ContentDomains.new(node, domain_scope_index:, included_domains: included_domain_scopes).to_a
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def domain_scope_index
|
|
130
|
+
@domain_scope_index ||= {}.compare_by_identity.tap do |scopes|
|
|
131
|
+
[ *setup_groups.keys, *test_groups.keys ].each { scopes[it] = true }
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def included_domain_scopes
|
|
136
|
+
@included_domain_scopes ||= domain_scope_index.keys.select do |scope|
|
|
137
|
+
any_block_type?(scope) && call_on_self?(call_of(scope)) && scope.method?(:included)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def write_groups
|
|
142
|
+
@write_groups ||= content_groups_of(nodes_in(ast, :ivasgn))
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def setup_callback_groups
|
|
146
|
+
@setup_callback_groups ||= groups_of(setup_callbacks)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def setup_callbacks
|
|
150
|
+
nodes_in(ast, :send, :csend).select { call_on_self?(it) && it.method?(:setup) }
|
|
151
|
+
.map { callback_node_for(it) }.sort_by { it.source_range.begin_pos }
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def callback_node_for(call)
|
|
155
|
+
if any_block_type?(call.parent) && call_of(call.parent).equal?(call)
|
|
156
|
+
call.parent
|
|
157
|
+
else
|
|
158
|
+
call
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# A read or write belongs to the nearest active test domain, but an unopened class-like boundary owns its state
|
|
163
|
+
# too. Concern helpers outside `included` share that block's runtime target without crossing nested owners.
|
|
164
|
+
class ContentDomains
|
|
165
|
+
include RuboCop::Callbacksystems::Helpers
|
|
166
|
+
|
|
167
|
+
def initialize(node, domain_scope_index:, included_domains:)
|
|
168
|
+
@node = node
|
|
169
|
+
@domain_scope_index = domain_scope_index
|
|
170
|
+
@included_domains = included_domains
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def to_a
|
|
174
|
+
{}.compare_by_identity.tap do |domains|
|
|
175
|
+
primary_domain&.then { domains[it] = true }
|
|
176
|
+
related_included_domains.each { domains[it] = true }
|
|
177
|
+
end.keys
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
private
|
|
181
|
+
attr_reader :node, :domain_scope_index, :included_domains
|
|
182
|
+
|
|
183
|
+
def primary_domain
|
|
184
|
+
nearest_domain_or_boundary if domain_scope_index.key?(nearest_domain_or_boundary)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def nearest_domain_or_boundary
|
|
188
|
+
@nearest_domain_or_boundary ||= node.each_ancestor.find do |ancestor|
|
|
189
|
+
domain_scope_index.key?(ancestor) || test_domain_boundary?(ancestor)
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def test_domain_boundary?(candidate)
|
|
194
|
+
candidate.type?(:class, :module, :sclass) ||
|
|
195
|
+
(any_block_type?(candidate) && self_rebinding_boundary?(candidate))
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def related_included_domains
|
|
199
|
+
included_domains.select do |scope|
|
|
200
|
+
direct_owner_of(scope).equal?(direct_owner) &&
|
|
201
|
+
(!primary_domain || primary_domain.equal?(scope) || primary_domain.equal?(direct_owner))
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def direct_owner_of(subject)
|
|
206
|
+
subject.each_ancestor.find { test_domain_boundary?(it) }
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def direct_owner
|
|
210
|
+
@direct_owner ||= direct_owner_of(node)
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# One lexical test container, which excludes inherited setup and same-named variables in its neighbours.
|
|
216
|
+
class TestDomain
|
|
217
|
+
include RuboCop::Callbacksystems::Helpers
|
|
218
|
+
|
|
219
|
+
def initialize(scope, setups:, context:)
|
|
220
|
+
@scope = scope
|
|
221
|
+
@setups = setups
|
|
222
|
+
@context = context
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def each_offense
|
|
226
|
+
return if abstract_base_class?
|
|
227
|
+
|
|
228
|
+
setups.each { setup_analysis_for(it).each_offense { yield it } }
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
private
|
|
232
|
+
attr_reader :scope, :setups, :context
|
|
233
|
+
|
|
234
|
+
delegate :tests, to: :context, private: true
|
|
235
|
+
|
|
236
|
+
# A base class with no tests of its own hands its setup to subclasses that may live in other files.
|
|
237
|
+
def abstract_base_class?
|
|
238
|
+
scope.class_type? && rails_test_base_class?(scope.parent_class) && tests.empty?
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def setup_analysis_for(setup)
|
|
242
|
+
SetupBlock.new(setup, assignments: unambiguous_assignments.fetch(setup), context:)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def unambiguous_assignments
|
|
246
|
+
@unambiguous_assignments ||= assignments_by_setup.transform_values do |assignments|
|
|
247
|
+
assignments.reject { repeated_names.include?(it.name) }
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def assignments_by_setup
|
|
252
|
+
@assignments_by_setup ||= {}.compare_by_identity.tap do |grouped|
|
|
253
|
+
setups.each { grouped[it] = SetupAssignments.new(it.body, boundary: it).to_a }
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def repeated_names
|
|
258
|
+
@repeated_names ||= assignments_by_setup.values.flatten.group_by(&:name)
|
|
259
|
+
.select { |_name, assignments| assignments.many? }.keys
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
class SetupBlock
|
|
264
|
+
include RuboCop::Callbacksystems::Helpers
|
|
265
|
+
|
|
266
|
+
def initialize(setup, assignments:, context:)
|
|
267
|
+
@setup = setup
|
|
268
|
+
@assignments = assignments
|
|
269
|
+
@context = context
|
|
270
|
+
@tooling_protection = ToolingProtection.new(setup, assignments, context.processed_source)
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def each_offense
|
|
274
|
+
offending.each { yield offense_for(it) }
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
private
|
|
278
|
+
attr_reader :setup, :assignments, :context, :tooling_protection
|
|
279
|
+
|
|
280
|
+
def offending
|
|
281
|
+
@offending ||= assignments.map { assignment_for(it) }.select(&:offending?)
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def assignment_for(node)
|
|
285
|
+
SetupAssignment.new(node, setup:, context:, tooling_protection:)
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def offense_for(assignment)
|
|
289
|
+
if assignment.correctable?
|
|
290
|
+
RuboCop::Callbacksystems::Offense.new(assignment.node, assignment.message) { correct(assignment, it) }
|
|
291
|
+
else
|
|
292
|
+
RuboCop::Callbacksystems::Offense.new(assignment.node, assignment.message)
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def correct(assignment, corrector)
|
|
297
|
+
assignment.correct(corrector)
|
|
298
|
+
removal_range_for(assignment)&.then { corrector.remove(it) }
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def removal_range_for(assignment)
|
|
302
|
+
if cleared?
|
|
303
|
+
block_removal_range if assignment.equal?(first_removable)
|
|
304
|
+
elsif assignment.removable?
|
|
305
|
+
statement_removal_range_for(assignment.node)
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def cleared?
|
|
310
|
+
removable.size == statements_in(setup.body).size
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def removable
|
|
314
|
+
@removable ||= offending.select(&:removable?)
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def first_removable
|
|
318
|
+
removable.min_by { it.node.source_range.begin_pos }
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# The blank line below the block goes too, so the first test keeps its distance from the line above, and the
|
|
322
|
+
# removal of a block written right below takes its own blank line instead of sharing this one.
|
|
323
|
+
def block_removal_range
|
|
324
|
+
inline_statement_removal_range_for(setup) || standalone_block_removal_range
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def standalone_block_removal_range
|
|
328
|
+
setup.source_range.with(begin_pos: line_start_position_of(setup), end_pos: position_past_trailing_blank)
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def position_past_trailing_blank
|
|
332
|
+
position_past_newline_at(position_past_newline_at(setup.source_range.end_pos))
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def position_past_newline_at(position)
|
|
336
|
+
newline_at?(position) ? position + 1 : position
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def newline_at?(position)
|
|
340
|
+
setup.source_range.source_buffer.source[position] == "\n"
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# Tooling comments cannot move with an assignment. A comment belonging to one assignment protects that assignment;
|
|
345
|
+
# one belonging to the setup itself protects them all because removing the callback would change its scope.
|
|
346
|
+
class ToolingProtection
|
|
347
|
+
include RuboCop::Callbacksystems::Helpers
|
|
348
|
+
|
|
349
|
+
def initialize(setup, assignments, processed_source)
|
|
350
|
+
@setup = setup
|
|
351
|
+
@assignments = assignments
|
|
352
|
+
@processed_source = processed_source
|
|
353
|
+
@source_comments = RuboCop::Callbacksystems::Source::Comments.for(processed_source)
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def protects?(assignment)
|
|
357
|
+
unassigned_tooling? || assignment_block_for(assignment).contains_tooling_comment?
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
private
|
|
361
|
+
attr_reader :setup, :assignments, :processed_source, :source_comments
|
|
362
|
+
|
|
363
|
+
def unassigned_tooling?
|
|
364
|
+
tooling_comments.any? do |comment|
|
|
365
|
+
assignment_blocks.values.none? { it.range.contains?(comment.source_range) }
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def tooling_comments
|
|
370
|
+
@tooling_comments ||= source_comments.within(setup_block.range).select { tooling_comment?(it) }
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
def setup_block
|
|
374
|
+
@setup_block ||= RuboCop::Callbacksystems::Source::StatementWithComments.new(setup, processed_source)
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def assignment_blocks
|
|
378
|
+
@assignment_blocks ||= {}.compare_by_identity.tap do |blocks|
|
|
379
|
+
assignments.each do |assignment|
|
|
380
|
+
blocks[assignment] = RuboCop::Callbacksystems::Source::StatementWithComments.new \
|
|
381
|
+
assignment, processed_source
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def assignment_block_for(assignment)
|
|
387
|
+
assignment_blocks.fetch(assignment)
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
class SetupAssignment
|
|
392
|
+
include RuboCop::Callbacksystems::Helpers
|
|
393
|
+
|
|
394
|
+
USED_ONCE_MESSAGE = "Instance variable `%<variable>s` is only used in one test. Inline it instead of assigning " \
|
|
395
|
+
"in `setup`."
|
|
396
|
+
UNUSED_MESSAGE = "Instance variable `%<variable>s` assigned in `setup` is not used by any test. Remove the " \
|
|
397
|
+
"assignment."
|
|
398
|
+
INLINABLE_TYPES = %i[
|
|
399
|
+
int float rational complex str dstr sym dsym regexp true false nil self array hash const lvar ivar cvar gvar
|
|
400
|
+
]
|
|
401
|
+
|
|
402
|
+
attr_reader :node
|
|
403
|
+
|
|
404
|
+
def initialize(node, setup:, context:, tooling_protection:)
|
|
405
|
+
@node = node
|
|
406
|
+
@setup = setup
|
|
407
|
+
@context = context
|
|
408
|
+
@tooling_protection = tooling_protection
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
def offending?
|
|
412
|
+
usage.unused? || usage.single_use?
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
def message
|
|
416
|
+
format(usage.unused? ? UNUSED_MESSAGE : USED_ONCE_MESSAGE, variable: node.name)
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
def correct(corrector)
|
|
420
|
+
if inline_target
|
|
421
|
+
replace_expression(corrector, inline_target, with: value.source)
|
|
422
|
+
elsif usage.unused? && !discardable_value?
|
|
423
|
+
corrector.replace(node, value.source)
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
def correctable?
|
|
428
|
+
removable? || replaceable_unused_value?
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
def removable?
|
|
432
|
+
source_self_contained? && !tooling_protection.protects?(node) && direct_statement? &&
|
|
433
|
+
(inline_target || (usage.unused? && discardable_value?))
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
private
|
|
437
|
+
attr_reader :setup, :context, :tooling_protection
|
|
438
|
+
|
|
439
|
+
delegate :tests, :writes, to: :context, private: true
|
|
440
|
+
|
|
441
|
+
def usage
|
|
442
|
+
@usage ||= context.usage_of(node.name)
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
def inline_target
|
|
446
|
+
usage.sole_direct_read if directly_inlinable?
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
def directly_inlinable?
|
|
450
|
+
direct_statement? && stable_single_use? && relocation_safe?
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
def direct_statement?
|
|
454
|
+
statements_in(setup.body).any? { it.equal?(node) }
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
def stable_single_use?
|
|
458
|
+
sole_write? && single_use_of_inlinable_value?
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
def sole_write?
|
|
462
|
+
writes.one? { it.name == node.name }
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
def single_use_of_inlinable_value?
|
|
466
|
+
usage.single_use? && inlinable_value?
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
def inlinable_value?
|
|
470
|
+
INLINABLE_TYPES.include?(value.type) || primary_call?
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
def value
|
|
474
|
+
node.expression
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
def primary_call?
|
|
478
|
+
value.call_type? && plain_method? && primary_shape?
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
def plain_method?
|
|
482
|
+
!value.operator_method? && !value.comparison_method?
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
def primary_shape?
|
|
486
|
+
value.arguments.empty? || value.parenthesized?
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
def relocation_safe?
|
|
490
|
+
value.recursive_basic_literal? || relocation.safe?
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
def relocation
|
|
494
|
+
@relocation ||= ValueRelocation.new(node, setup:, context:, read: usage.sole_direct_read)
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
def discardable_value?
|
|
498
|
+
value.recursive_basic_literal?
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
def source_self_contained?
|
|
502
|
+
!carries_heredoc?(node)
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
def replaceable_unused_value?
|
|
506
|
+
direct_statement? && usage.unused? && !discardable_value?
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
# Moving an evaluated value between callbacks is only offered when every ordering fact visible in this file is
|
|
510
|
+
# preserved. Calls remain intrinsically unsafe because external callbacks and their side effects are unknowable.
|
|
511
|
+
class ValueRelocation
|
|
512
|
+
include RuboCop::Callbacksystems::Helpers
|
|
513
|
+
|
|
514
|
+
LAZY_ANCESTOR_TYPES = %i[
|
|
515
|
+
and case case_match csend defined? ensure for if in_pattern or rescue resbody
|
|
516
|
+
until until_post while while_post
|
|
517
|
+
]
|
|
518
|
+
LOCAL_BINDING_TYPES = %i[ lvar lvasgn match_var ]
|
|
519
|
+
|
|
520
|
+
def initialize(assignment, setup:, context:, read:)
|
|
521
|
+
@assignment = assignment
|
|
522
|
+
@setup = setup
|
|
523
|
+
@context = context
|
|
524
|
+
@read = read
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
def safe?
|
|
528
|
+
read && keeps_scope? && follows_visible_setup_work? && eagerly_leads_test_work?
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
private
|
|
532
|
+
attr_reader :assignment, :setup, :context, :read
|
|
533
|
+
|
|
534
|
+
delegate :setup_callbacks, :tests, to: :context, private: true
|
|
535
|
+
|
|
536
|
+
def keeps_scope?
|
|
537
|
+
nodes_in(assignment.expression, *LOCAL_BINDING_TYPES).empty?
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
def follows_visible_setup_work?
|
|
541
|
+
setup_callbacks.last.equal?(setup) && statements_in(setup.body).last.equal?(assignment)
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
def eagerly_leads_test_work?
|
|
545
|
+
tests.any? { it.equal?(test) } && lazy_ancestors.empty? && prior_effects.empty?
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
def test
|
|
549
|
+
@test ||= read.each_ancestor(:any_block).first
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
def lazy_ancestors
|
|
553
|
+
evaluation_path.select { it.type?(*LAZY_ANCESTOR_TYPES) }
|
|
554
|
+
end
|
|
555
|
+
|
|
556
|
+
def evaluation_path
|
|
557
|
+
@evaluation_path ||= read.each_ancestor.take_while { !it.equal?(test) }
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
def prior_effects
|
|
561
|
+
current = read
|
|
562
|
+
evaluation_path.flat_map do |ancestor|
|
|
563
|
+
ancestor.child_nodes.take_while { !it.equal?(current) }.reject(&:recursive_basic_literal?).tap do
|
|
564
|
+
current = ancestor
|
|
565
|
+
end
|
|
566
|
+
end
|
|
567
|
+
end
|
|
568
|
+
end
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
# All reads in a test domain are indexed once, then every setup assignment asks only for its own variable.
|
|
572
|
+
class InstanceVariableUsages
|
|
573
|
+
def initialize(reads, tests:)
|
|
574
|
+
@test_membership = TestMembership.new(tests)
|
|
575
|
+
@reads_by_name = {}
|
|
576
|
+
@usages = {}
|
|
577
|
+
|
|
578
|
+
reads.each do |read|
|
|
579
|
+
(reads_by_name[read.name] ||= []) << read
|
|
580
|
+
test_membership.index(read)
|
|
581
|
+
end
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
def for(variable_name)
|
|
585
|
+
usages[variable_name] ||= InstanceVariableUsage.new(reads_by_name.fetch(variable_name, []), test_membership:)
|
|
586
|
+
end
|
|
587
|
+
|
|
588
|
+
private
|
|
589
|
+
attr_reader :reads_by_name, :test_membership, :usages
|
|
590
|
+
|
|
591
|
+
class TestMembership
|
|
592
|
+
Membership = Data.define(:tests, :direct)
|
|
593
|
+
|
|
594
|
+
def initialize(tests)
|
|
595
|
+
@test_index = {}.compare_by_identity
|
|
596
|
+
@memberships = {}.compare_by_identity
|
|
597
|
+
tests.each { test_index[it] = true }
|
|
598
|
+
end
|
|
599
|
+
|
|
600
|
+
def index(read)
|
|
601
|
+
memberships[read] = membership_of(read)
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
def all_inside?(reads)
|
|
605
|
+
reads.all? { memberships.fetch(it).tests.any? }
|
|
606
|
+
end
|
|
607
|
+
|
|
608
|
+
def one_test_for?(reads)
|
|
609
|
+
reads.each_with_object({}.compare_by_identity) do |read, reading_tests|
|
|
610
|
+
memberships.fetch(read).tests.each { reading_tests[it] = true }
|
|
611
|
+
end.one?
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
def direct?(read)
|
|
615
|
+
memberships.fetch(read).direct
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
private
|
|
619
|
+
attr_reader :memberships, :test_index
|
|
620
|
+
|
|
621
|
+
def membership_of(read)
|
|
622
|
+
nearest_block = nil
|
|
623
|
+
tests = read.each_ancestor(:any_block).with_object([]) do |block, containing_tests|
|
|
624
|
+
nearest_block ||= block
|
|
625
|
+
containing_tests << block if test_index.key?(block)
|
|
626
|
+
end
|
|
627
|
+
|
|
628
|
+
Membership.new(tests, test_index.key?(nearest_block))
|
|
629
|
+
end
|
|
630
|
+
end
|
|
631
|
+
end
|
|
632
|
+
|
|
633
|
+
class InstanceVariableUsage
|
|
634
|
+
def initialize(reads, test_membership:)
|
|
635
|
+
@reads = reads
|
|
636
|
+
@test_membership = test_membership
|
|
637
|
+
end
|
|
638
|
+
|
|
639
|
+
def unused?
|
|
640
|
+
reads.none?
|
|
641
|
+
end
|
|
642
|
+
|
|
643
|
+
# A read outside the tests, in a helper or a teardown, keeps the variable shared however few tests read it.
|
|
644
|
+
def single_use?
|
|
645
|
+
test_membership.all_inside?(reads) && test_membership.one_test_for?(reads)
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
# A read inside a block of its test would run the value again, so only a direct read counts as the one read.
|
|
649
|
+
def sole_direct_read
|
|
650
|
+
sole_read if sole_read && test_membership.direct?(sole_read)
|
|
651
|
+
end
|
|
652
|
+
|
|
653
|
+
private
|
|
654
|
+
attr_reader :reads, :test_membership
|
|
655
|
+
|
|
656
|
+
def sole_read
|
|
657
|
+
reads.first if reads.one?
|
|
658
|
+
end
|
|
659
|
+
end
|
|
660
|
+
|
|
661
|
+
# Instance-variable assignments executed by a setup, without descending through a deferred lexical boundary.
|
|
662
|
+
class SetupAssignments
|
|
663
|
+
include Enumerable
|
|
664
|
+
include RuboCop::Callbacksystems::Helpers
|
|
665
|
+
|
|
666
|
+
def initialize(body, boundary: nil)
|
|
667
|
+
@body = body
|
|
668
|
+
@boundary = boundary
|
|
669
|
+
end
|
|
670
|
+
|
|
671
|
+
def each
|
|
672
|
+
assignments.each { yield it } if block_given?
|
|
673
|
+
to_enum(__method__) unless block_given?
|
|
674
|
+
end
|
|
675
|
+
|
|
676
|
+
private
|
|
677
|
+
attr_reader :body, :boundary
|
|
678
|
+
|
|
679
|
+
def assignments
|
|
680
|
+
RuboCop::Callbacksystems::Execution::Immediate.new(body).nodes_of_type(:ivasgn)
|
|
681
|
+
.select { it.expression && self_preserved_between?(it, boundary:) }
|
|
682
|
+
end
|
|
683
|
+
end
|
|
684
|
+
end
|