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,242 @@
|
|
|
1
|
+
# Detects methods that only delegate to a same-named method on a delegatable
|
|
2
|
+
# receiver (`def size; node.size; end`). The `delegate` macro says it
|
|
3
|
+
# declaratively, whether the receiver is a constant, a call on self or a chain
|
|
4
|
+
# of them.
|
|
5
|
+
#
|
|
6
|
+
# Rails/Delegate covers the public single-hop case, so what is left here is the
|
|
7
|
+
# private one (the macro defines public methods unless told otherwise), the
|
|
8
|
+
# nested one, where a chain of calls becomes a dotted target that Rails/Delegate
|
|
9
|
+
# does not recognize, and the one that forwards a block, which it does not read
|
|
10
|
+
# because it matches positional arguments only. The macro forwards the block on
|
|
11
|
+
# its own.
|
|
12
|
+
#
|
|
13
|
+
# A private method taking no parameters reads its arguments from its own state,
|
|
14
|
+
# so delegating it changes arity and every call site has to start passing them,
|
|
15
|
+
# which is why the offense is only reported. The same public method is left
|
|
16
|
+
# alone, since its callers are not all in view and a command line is one of
|
|
17
|
+
# them. So is a method that combines its own parameters with data of its own,
|
|
18
|
+
# a partial application that every call site would have to repeat.
|
|
19
|
+
#
|
|
20
|
+
# The autocorrection folds the method into an existing same-target `delegate`
|
|
21
|
+
# when one is present, keeping them on one line, and otherwise writes a fresh
|
|
22
|
+
# macro where the section already declares things, below its mixins when it
|
|
23
|
+
# declares nothing yet.
|
|
24
|
+
#
|
|
25
|
+
# @example
|
|
26
|
+
# # bad - hand-written private delegation
|
|
27
|
+
# private
|
|
28
|
+
# delegate :name, to: :node, private: true
|
|
29
|
+
#
|
|
30
|
+
# def size
|
|
31
|
+
# node.size
|
|
32
|
+
# end
|
|
33
|
+
#
|
|
34
|
+
# # good - folded into the existing delegate
|
|
35
|
+
# private
|
|
36
|
+
# delegate :name, :size, to: :node, private: true
|
|
37
|
+
#
|
|
38
|
+
# # bad - hand-written nested delegation
|
|
39
|
+
# def database_names
|
|
40
|
+
# config.postgres.database_names
|
|
41
|
+
# end
|
|
42
|
+
#
|
|
43
|
+
# # good - the chain becomes a dotted target
|
|
44
|
+
# delegate :database_names, to: "config.postgres"
|
|
45
|
+
#
|
|
46
|
+
# # bad - the method exists only to hand the block over
|
|
47
|
+
# def each(&block)
|
|
48
|
+
# blocks.each(&block)
|
|
49
|
+
# end
|
|
50
|
+
#
|
|
51
|
+
# # good
|
|
52
|
+
# delegate :each, to: :blocks
|
|
53
|
+
#
|
|
54
|
+
# # bad - callers must pass the argument: subunit_factor(options[:currency])
|
|
55
|
+
# private
|
|
56
|
+
# def subunit_factor
|
|
57
|
+
# Currency.subunit_factor(options[:currency])
|
|
58
|
+
# end
|
|
59
|
+
#
|
|
60
|
+
# # good - delegating would make every call site repeat the slot names
|
|
61
|
+
# def parse(raw)
|
|
62
|
+
# WALPressure.parse(raw, slot_names: SLOTS)
|
|
63
|
+
# end
|
|
64
|
+
#
|
|
65
|
+
class RuboCop::Cop::Callbacksystems::PreferDelegate < RuboCop::Cop::Callbacksystems::Base
|
|
66
|
+
extend RuboCop::Cop::AutoCorrector
|
|
67
|
+
|
|
68
|
+
def on_new_investigation
|
|
69
|
+
@source_comments = RuboCop::Callbacksystems::Source::Comments.for(processed_source)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def on_def(node)
|
|
73
|
+
report ManualDelegation.new(node, @source_comments)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
class ManualDelegation
|
|
78
|
+
include RuboCop::Callbacksystems::Helpers
|
|
79
|
+
|
|
80
|
+
MESSAGE = "Use `%<macro>s` instead of a hand-written delegation%<arguments_note>s."
|
|
81
|
+
|
|
82
|
+
def initialize(node, source_comments)
|
|
83
|
+
@node = node
|
|
84
|
+
@source_comments = source_comments
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def offense
|
|
88
|
+
if declarable?
|
|
89
|
+
RuboCop::Callbacksystems::Offense.new(node, message, correcting: correctable?) { correct(it) }
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
attr_reader :node, :source_comments
|
|
95
|
+
delegate :body, to: :node, private: true
|
|
96
|
+
|
|
97
|
+
def declarable?
|
|
98
|
+
declarable_delegation? && (forwards_own_parameters? ? beyond_rails_delegate? : binds_own_state?)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# `delegate` declares public and private methods, so a protected one has no macro to stand in its place.
|
|
102
|
+
def declarable_delegation?
|
|
103
|
+
body&.send_type? && body.method?(node.method_name) && target.delegable? &&
|
|
104
|
+
!protected_method?(node) && declared_in_class_body?
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def target
|
|
108
|
+
@target ||= RuboCop::Callbacksystems::Methods::DelegationTarget.new(body.receiver)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# A macro stands where the definition stands, so one nested in a conditional or a block has no place to give it.
|
|
112
|
+
def declared_in_class_body?
|
|
113
|
+
statements_in(enclosing_definition_of(node)&.body).any? { it.equal?(statement) }
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def statement
|
|
117
|
+
@statement ||= visibility_applied_to(node.parent, node) ? node.parent : node
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def forwards_own_parameters?
|
|
121
|
+
body.arguments.map(&:source) == node.arguments.map(&:source)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def beyond_rails_delegate?
|
|
125
|
+
private_method?(node) || target.nested? || forwards_block?
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def forwards_block?
|
|
129
|
+
node.arguments.any?(&:blockarg_type?)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def binds_own_state?
|
|
133
|
+
node.arguments.empty? && private_method?(node)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def message
|
|
137
|
+
format MESSAGE,
|
|
138
|
+
macro: conversion.macro_source,
|
|
139
|
+
arguments_note: forwards_own_parameters? ? "" : " and pass the arguments at the call sites"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def conversion
|
|
143
|
+
@conversion ||= Conversion.new(node, target, source_comments)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def correctable?
|
|
147
|
+
forwards_own_parameters? && conversion.placeable?
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def correct(corrector)
|
|
151
|
+
conversion.apply(corrector)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
class Conversion
|
|
156
|
+
include RuboCop::Callbacksystems::Helpers
|
|
157
|
+
|
|
158
|
+
def initialize(node, target, source_comments)
|
|
159
|
+
@node = node
|
|
160
|
+
@target = target
|
|
161
|
+
@source_comments = source_comments
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def placeable?
|
|
165
|
+
(sibling_delegate.present? || anchor.present?) && !comments_attached_to_definition?
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def apply(corrector)
|
|
169
|
+
write(corrector)
|
|
170
|
+
corrector.remove(statement_removal_range_for(node))
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def macro_source
|
|
174
|
+
"delegate :#{node.method_name}, to: #{target.source}#{private_option}"
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
private
|
|
178
|
+
attr_reader :node, :target, :source_comments
|
|
179
|
+
|
|
180
|
+
def sibling_delegate
|
|
181
|
+
@sibling_delegate ||= container_macros.find { sibling?(it) }
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def container_macros
|
|
185
|
+
container_statements.map { RuboCop::Callbacksystems::Methods::DelegateMacro.new(it) }
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def container_statements
|
|
189
|
+
statements_in(enclosing_definition_of(node).body)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def sibling?(macro)
|
|
193
|
+
macro.macro? && macro.target == target.name && macro.plain?(private: private_delegation?)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def private_delegation?
|
|
197
|
+
private_method?(node)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def anchor
|
|
201
|
+
@anchor ||= declarations.last || private_modifier || mixins.last
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def declarations
|
|
205
|
+
preceding_statements.select { declaration_macro?(it) }
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def preceding_statements
|
|
209
|
+
container_statements.take_while { it != node }
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def private_modifier
|
|
213
|
+
preceding_statements.find { visibility_modifier_of(it) == :private }
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def mixins
|
|
217
|
+
preceding_statements.select { mixin_macro?(it) }
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def comments_attached_to_definition?
|
|
221
|
+
source_comments.any_on_lines?(node.first_line..node.last_line) ||
|
|
222
|
+
source_comments.own_line_comment_on(node.first_line - 1).present?
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def write(corrector)
|
|
226
|
+
if sibling_delegate
|
|
227
|
+
corrector.insert_before(sibling_delegate.first_option, ":#{node.method_name}, ")
|
|
228
|
+
else
|
|
229
|
+
corrector.insert_after(anchor, "#{separator}#{indentation_of(node)}#{macro_source}")
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# After a mixin the macro keeps the blank line that belongs there, and elsewhere it joins the line above.
|
|
234
|
+
def separator
|
|
235
|
+
mixins.any? { it.equal?(anchor) } ? "\n\n" : "\n"
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def private_option
|
|
239
|
+
", private: true" if private_delegation?
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# Detects `@x = receiver.x` style assignments inside `initialize` that mirror
|
|
2
|
+
# a method call. These eager assignments are better expressed as `delegate`
|
|
3
|
+
# declarations: they read declaratively, avoid duplicating the name, and stop
|
|
4
|
+
# inviting bigger refactors (the very anti-pattern flagged by
|
|
5
|
+
# `NoRedundantWrapperMethod` and `NoAnemicDelegation`).
|
|
6
|
+
#
|
|
7
|
+
# Only assignments where the variable name matches the called method name are
|
|
8
|
+
# flagged, and only when the receiver itself is also assigned to a variable of
|
|
9
|
+
# the same name in the same `initialize` (so the receiver is reachable via
|
|
10
|
+
# `attr_reader` from the surrounding class).
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
# # bad - eager assignment that mirrors `node.body`
|
|
14
|
+
# def initialize(node)
|
|
15
|
+
# @node = node
|
|
16
|
+
# @body = node.body
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# private
|
|
20
|
+
# attr_reader :node, :body
|
|
21
|
+
#
|
|
22
|
+
# # good - declare the delegation
|
|
23
|
+
# def initialize(node)
|
|
24
|
+
# @node = node
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
# private
|
|
28
|
+
# attr_reader :node
|
|
29
|
+
# delegate :body, to: :node, private: true
|
|
30
|
+
#
|
|
31
|
+
# # ok - the assignment transforms the value (different name from the call)
|
|
32
|
+
# def initialize(name)
|
|
33
|
+
# @name = name
|
|
34
|
+
# @name_parts = name.split("_")
|
|
35
|
+
# end
|
|
36
|
+
#
|
|
37
|
+
class RuboCop::Cop::Callbacksystems::PreferDelegateOverInstanceVariableAssignment < RuboCop::Cop::Callbacksystems::Base
|
|
38
|
+
def on_ivasgn(node)
|
|
39
|
+
report Assignment.new(node)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
class Assignment
|
|
44
|
+
extend RuboCop::AST::NodePattern::Macros
|
|
45
|
+
include RuboCop::Callbacksystems::Helpers
|
|
46
|
+
|
|
47
|
+
MESSAGE = "Replace `@%<name>s = %<receiver>s.%<name>s` with `delegate :%<name>s, to: :%<receiver>s`."
|
|
48
|
+
|
|
49
|
+
# @!method mirroring_assignment?(node)
|
|
50
|
+
def_node_matcher :mirroring_assignment?, <<~PATTERN
|
|
51
|
+
(ivasgn $_ (send (lvar $_) $_))
|
|
52
|
+
PATTERN
|
|
53
|
+
|
|
54
|
+
# @!method receiver_assignment?(node, variable_name, lvar_name)
|
|
55
|
+
def_node_matcher :receiver_assignment?, <<~PATTERN
|
|
56
|
+
(ivasgn %1 (lvar %2))
|
|
57
|
+
PATTERN
|
|
58
|
+
|
|
59
|
+
def initialize(node)
|
|
60
|
+
@node = node
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def offense
|
|
64
|
+
RuboCop::Callbacksystems::Offense.new(node, message) if mirroring_delegation?
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
attr_reader :node
|
|
69
|
+
|
|
70
|
+
def mirroring_delegation?
|
|
71
|
+
variable_name && variable_matches_method? && stable_initialization?
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def variable_name
|
|
75
|
+
captures&.first
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def captures
|
|
79
|
+
@captures ||= mirroring_assignment?(node)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def variable_matches_method?
|
|
83
|
+
name_without_sigil(variable_name).to_sym == method_name
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def method_name
|
|
87
|
+
captures.third
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def stable_initialization?
|
|
91
|
+
direct_receiver_pair? && stable_state?
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def direct_receiver_pair?
|
|
95
|
+
direct_initialize_assignment? && receiver_assignment_precedes?
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def direct_initialize_assignment?
|
|
99
|
+
initialize_method && direct_statements.any? { it.equal?(node) }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def initialize_method
|
|
103
|
+
@initialize_method ||= node.each_ancestor(:any_def).find { it.method?(:initialize) }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def direct_statements
|
|
107
|
+
@direct_statements ||= statements_in(initialize_method.body)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def receiver_assignment_precedes?
|
|
111
|
+
receiver_assignment && receiver_assignment.source_range.begin_pos < node.source_range.begin_pos
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def receiver_assignment
|
|
115
|
+
@receiver_assignment ||= direct_statements.find do |statement|
|
|
116
|
+
receiver_assignment?(statement, :"@#{receiver_name}", receiver_name)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def receiver_name
|
|
121
|
+
captures.second
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def stable_state?
|
|
125
|
+
receiver_local_stable? && instance_state_stable?
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def receiver_local_stable?
|
|
129
|
+
receiver_rebindings.none? { between_receiver_and_mirror?(it) }
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def receiver_rebindings
|
|
133
|
+
nodes_in(initialize_method.body, :lvasgn, :match_var).select { variable_name_of(it) == receiver_name }
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def between_receiver_and_mirror?(write)
|
|
137
|
+
write.source_range.begin_pos.between?(receiver_assignment.source_range.end_pos, node.source_range.begin_pos)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def instance_state_stable?
|
|
141
|
+
sole_instance_write?(receiver_variable_name, receiver_assignment) && sole_instance_write?(variable_name, node)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def sole_instance_write?(name, expected)
|
|
145
|
+
instance_writes_to(name).then { it.one? && it.first.equal?(expected) }
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def instance_writes_to(name)
|
|
149
|
+
nodes_in(state_scope&.body, :ivasgn).select { it.name == name && same_instance?(it) }
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def state_scope
|
|
153
|
+
state_domain.container
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def state_domain
|
|
157
|
+
@state_domain ||= RuboCop::Callbacksystems::Methods::Domain.new(node)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def same_instance?(write)
|
|
161
|
+
domain = RuboCop::Callbacksystems::Methods::Domain.new(write)
|
|
162
|
+
|
|
163
|
+
domain.container.equal?(state_scope) && domain.identity == state_domain.identity &&
|
|
164
|
+
self_preserved_between?(write, boundary: enclosing_method_of(write) || state_scope)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def receiver_variable_name
|
|
168
|
+
:"@#{receiver_name}"
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def message
|
|
172
|
+
format(MESSAGE, name: method_name, receiver: receiver_name)
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# Detects array subtraction and record-ID exclusions that can use `excluding`.
|
|
2
|
+
# The method names the excluded object directly, instead of wrapping it in a
|
|
3
|
+
# throwaway array or spelling out its database identifier.
|
|
4
|
+
#
|
|
5
|
+
# `excluding` flattens the arguments it receives, and Enumerable supplies a
|
|
6
|
+
# version whose result is an Array. Automatic correction therefore needs
|
|
7
|
+
# positive evidence that the receiver is an Array and the element is not one;
|
|
8
|
+
# an unknown value is still reported for a human who knows its domain.
|
|
9
|
+
#
|
|
10
|
+
# Relation exclusions are suggestions for human review. Active Record requires
|
|
11
|
+
# an instance of the relation's model and excludes its primary key, which may
|
|
12
|
+
# differ from the `id` column. Visible mismatches and custom primary keys are
|
|
13
|
+
# left alone, as are safe navigation and compound negative conditions.
|
|
14
|
+
#
|
|
15
|
+
# @example
|
|
16
|
+
# # bad
|
|
17
|
+
# users - [admin]
|
|
18
|
+
# items - [first_item]
|
|
19
|
+
# User.where.not(id: user.id)
|
|
20
|
+
#
|
|
21
|
+
# # good
|
|
22
|
+
# users.excluding(admin)
|
|
23
|
+
# items.excluding(first_item)
|
|
24
|
+
# User.excluding(user)
|
|
25
|
+
#
|
|
26
|
+
class RuboCop::Cop::Callbacksystems::PreferExcluding < RuboCop::Cop::Callbacksystems::Base
|
|
27
|
+
extend RuboCop::Cop::AutoCorrector
|
|
28
|
+
|
|
29
|
+
def on_send(node)
|
|
30
|
+
report Subtraction.new(node, source_comments:)
|
|
31
|
+
report RecordExclusion.new(node, source_root: processed_source.ast)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
alias on_csend on_send
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
class Subtraction
|
|
38
|
+
include RuboCop::Callbacksystems::Helpers
|
|
39
|
+
extend RuboCop::AST::NodePattern::Macros
|
|
40
|
+
|
|
41
|
+
MESSAGE = "Use `excluding` instead of `- [element]`."
|
|
42
|
+
NON_ARRAY_LITERAL_TYPES =
|
|
43
|
+
%i[ complex dstr dsym erange false float hash int irange nil rational regexp str sym true ]
|
|
44
|
+
|
|
45
|
+
# @!method single_element_subtraction(node)
|
|
46
|
+
def_node_matcher :single_element_subtraction, <<~PATTERN
|
|
47
|
+
(call $_ :- (array $_element))
|
|
48
|
+
PATTERN
|
|
49
|
+
|
|
50
|
+
def initialize(node, source_comments:)
|
|
51
|
+
@node = node
|
|
52
|
+
@source_comments = source_comments
|
|
53
|
+
@receiver, @element = single_element_subtraction(node)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def offense
|
|
57
|
+
if receiver
|
|
58
|
+
RuboCop::Callbacksystems::Offense.new(node, MESSAGE, correcting: correction_equivalent?) { correct(it) }
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
attr_reader :node, :source_comments, :receiver, :element
|
|
64
|
+
|
|
65
|
+
def correction_equivalent?
|
|
66
|
+
array_value?(receiver) && non_array_value?(element) && standalone_element_source? &&
|
|
67
|
+
!source_comments.any_within?(node)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def array_value?(candidate)
|
|
71
|
+
reads_as_array?(candidate) || adjacent_assigned_value_of(candidate)&.then { reads_as_array?(it) }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def adjacent_assigned_value_of(candidate)
|
|
75
|
+
AdjacentAssignment.new(candidate, beside: node).value
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def non_array_value?(candidate)
|
|
79
|
+
known_non_array?(candidate) || adjacent_assigned_value_of(candidate)&.then { known_non_array?(it) }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def known_non_array?(candidate)
|
|
83
|
+
NON_ARRAY_LITERAL_TYPES.include?(candidate.type) ||
|
|
84
|
+
(candidate.begin_type? && candidate.children.one? && known_non_array?(candidate.children.first))
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def standalone_element_source?
|
|
88
|
+
!element.parent.percent_literal? || element.type?(:str, :sym)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def correct(corrector)
|
|
92
|
+
corrector.replace(node, "#{receiver.source}#{call_operator}excluding(#{argument})")
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def call_operator
|
|
96
|
+
node.csend_type? ? "&." : "."
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Inside a percent literal an element is written bare, so its source alone would not survive on its own.
|
|
100
|
+
def argument
|
|
101
|
+
element.parent.percent_literal? ? element.value.inspect : element.source
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
class RecordExclusion
|
|
106
|
+
include RuboCop::Callbacksystems::Helpers
|
|
107
|
+
extend RuboCop::AST::NodePattern::Macros
|
|
108
|
+
|
|
109
|
+
MESSAGE = "Consider `excluding(%<record>s)` when the record belongs to this relation's model " \
|
|
110
|
+
"and its primary key is `id`."
|
|
111
|
+
|
|
112
|
+
# @!method record_exclusion(node)
|
|
113
|
+
def_node_matcher :record_exclusion, <<~PATTERN
|
|
114
|
+
(send (send $_relation :where) :not (hash (pair {(sym :id) (str "id")} (send $_record :id))))
|
|
115
|
+
PATTERN
|
|
116
|
+
|
|
117
|
+
def initialize(node, source_root:)
|
|
118
|
+
@node = node
|
|
119
|
+
@source_root = source_root
|
|
120
|
+
@relation, @record = record_exclusion(node)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def offense
|
|
124
|
+
if record && applicable?
|
|
125
|
+
RuboCop::Callbacksystems::Offense.new(node, format(MESSAGE, record: record.source))
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
private
|
|
130
|
+
attr_reader :node, :source_root, :relation, :record
|
|
131
|
+
|
|
132
|
+
def applicable?
|
|
133
|
+
node.each_node(:csend).none? && !non_record_value? && !different_models? && !custom_primary_key?
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def non_record_value?
|
|
137
|
+
record_value.literal? || record_value.const_type? || relation&.literal?
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def record_value
|
|
141
|
+
@record_value ||= AdjacentAssignment.new(record, beside: node).value || record
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def different_models?
|
|
145
|
+
relation_model && record_model && relation_model != record_model
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def relation_model
|
|
149
|
+
@relation_model ||= model_name_of(relation)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def model_name_of(candidate)
|
|
153
|
+
candidate = candidate.receiver while candidate&.call_type?
|
|
154
|
+
constant_name_of(candidate)&.delete_prefix("::")
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def record_model
|
|
158
|
+
@record_model ||= model_name_of(record_value)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def custom_primary_key?
|
|
162
|
+
source_root.each_node(:send, :defs).any? do |candidate|
|
|
163
|
+
if candidate.defs_type?
|
|
164
|
+
candidate.method?(:primary_key)
|
|
165
|
+
else
|
|
166
|
+
candidate.method?(:primary_key=) && !default_primary_key?(candidate.first_argument)
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def default_primary_key?(value)
|
|
172
|
+
value.type?(:str, :sym) && value.value.to_s == "id"
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# A local's immediately preceding assignment, shared by both forms of exclusion.
|
|
177
|
+
class AdjacentAssignment
|
|
178
|
+
def initialize(variable, beside:)
|
|
179
|
+
@variable = variable
|
|
180
|
+
@node = beside
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def value
|
|
184
|
+
if variable.lvar_type? && matching_assignment?
|
|
185
|
+
previous.expression
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
private
|
|
190
|
+
attr_reader :variable, :node
|
|
191
|
+
|
|
192
|
+
def matching_assignment?
|
|
193
|
+
previous&.lvasgn_type? && previous.name == variable.name
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def previous
|
|
197
|
+
@previous ||= statement_containing_node&.left_sibling
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def statement_containing_node
|
|
201
|
+
@statement_containing_node ||= [ node, *node.each_ancestor ].find { it.parent&.type?(:begin, :kwbegin) }
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Prefers `extract_associated` when a model query preloads one association and
|
|
2
|
+
# then collects that same association. Active Record supplies this exact
|
|
3
|
+
# operation as one method, keeping the association's name in one place.
|
|
4
|
+
#
|
|
5
|
+
# The receiver is assumed to be an Active Record relation. Comments prevent
|
|
6
|
+
# autocorrection, and a guarded preload followed by an unguarded map is left
|
|
7
|
+
# alone because collapsing it would change what happens when the receiver is nil.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# # bad
|
|
11
|
+
# posts.preload(:author).map(&:author)
|
|
12
|
+
# memberships.preload(:user).collect(&:user)
|
|
13
|
+
#
|
|
14
|
+
# # good
|
|
15
|
+
# posts.extract_associated(:author)
|
|
16
|
+
# memberships.extract_associated(:user)
|
|
17
|
+
#
|
|
18
|
+
class RuboCop::Cop::Callbacksystems::PreferExtractAssociated < RuboCop::Cop::Callbacksystems::Base
|
|
19
|
+
extend RuboCop::Cop::AutoCorrector
|
|
20
|
+
|
|
21
|
+
MESSAGE = "Use `extract_associated(%<association>s)` instead of preloading and mapping the same association."
|
|
22
|
+
|
|
23
|
+
# @!method association_extraction(node)
|
|
24
|
+
def_node_matcher :association_extraction, <<~PATTERN
|
|
25
|
+
(call $(call _ :preload $(sym _association)) {:map :collect} (block_pass (sym _association)))
|
|
26
|
+
PATTERN
|
|
27
|
+
|
|
28
|
+
def on_send(node)
|
|
29
|
+
association_extraction(node) do |preload, association|
|
|
30
|
+
next if preload.csend_type? && node.send_type?
|
|
31
|
+
|
|
32
|
+
add_extraction_offense(node, preload:, association:)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
alias on_csend on_send
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
def add_extraction_offense(node, preload:, association:)
|
|
40
|
+
message = format(MESSAGE, association: association.source)
|
|
41
|
+
|
|
42
|
+
if source_comments.any_within?(node)
|
|
43
|
+
add_offense(node, message:)
|
|
44
|
+
else
|
|
45
|
+
add_offense(node, message:) do |corrector|
|
|
46
|
+
corrector.replace(node, replacement_for(preload, association:))
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def replacement_for(preload, association:)
|
|
52
|
+
"#{call_prefix_of(preload)}extract_associated(#{association.source})"
|
|
53
|
+
end
|
|
54
|
+
end
|