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,247 @@
|
|
|
1
|
+
# A nested class with no body declares that something exists, most often an
|
|
2
|
+
# error. Written as a constant it says so in one line and stops looking like a
|
|
3
|
+
# class that lost its body.
|
|
4
|
+
#
|
|
5
|
+
# Only nested definitions are reported. A class living in its own file is that
|
|
6
|
+
# file's subject, and `class` reads as the heading it is.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# # bad
|
|
10
|
+
# class Configuration
|
|
11
|
+
# class Error < StandardError; end
|
|
12
|
+
#
|
|
13
|
+
# def call
|
|
14
|
+
# end
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# # good
|
|
18
|
+
# class Configuration
|
|
19
|
+
# Error = Class.new(StandardError)
|
|
20
|
+
#
|
|
21
|
+
# def call
|
|
22
|
+
# end
|
|
23
|
+
# end
|
|
24
|
+
#
|
|
25
|
+
class RuboCop::Cop::Callbacksystems::PreferClassNewForEmptyNestedClass < RuboCop::Cop::Callbacksystems::Base
|
|
26
|
+
include RuboCop::Callbacksystems::ProjectIndex::Support
|
|
27
|
+
extend RuboCop::Cop::AutoCorrector
|
|
28
|
+
|
|
29
|
+
def on_class(node)
|
|
30
|
+
report EmptyNestedClass.new \
|
|
31
|
+
node,
|
|
32
|
+
RuboCop::Callbacksystems::Source::Comments.for(processed_source),
|
|
33
|
+
assignment_safe: assignment_safe?(node)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
def assignment_safe?(node)
|
|
38
|
+
if node.identifier.namespace.nil? && project_index_reliable?
|
|
39
|
+
declaration = resolve_constant_in_index(node.identifier)
|
|
40
|
+
|
|
41
|
+
declaration.is_a?(Rubydex::Class) && DeclarationHistory.new(
|
|
42
|
+
declaration,
|
|
43
|
+
node,
|
|
44
|
+
file: processed_source.file_path
|
|
45
|
+
).new_at_node?
|
|
46
|
+
else
|
|
47
|
+
false
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# A class declaration can become an assignment only when this node creates the constant. Later declarations in the
|
|
52
|
+
# same file still see that class, while an earlier or externally ordered definition could be overwritten.
|
|
53
|
+
class DeclarationHistory
|
|
54
|
+
def initialize(declaration, node, file:)
|
|
55
|
+
@declaration = declaration
|
|
56
|
+
@node = node
|
|
57
|
+
@file = file
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def new_at_node?
|
|
61
|
+
current_definition && owners_known? && definitions.all? do |definition|
|
|
62
|
+
definition.equal?(current_definition) || later_in_current_file?(definition)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
attr_reader :declaration, :node, :file
|
|
68
|
+
|
|
69
|
+
def current_definition
|
|
70
|
+
@current_definition ||= definitions.find do |definition|
|
|
71
|
+
definition.is_a?(Rubydex::ClassDefinition) && same_location?(definition.location, node.source_range)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def definitions
|
|
76
|
+
@definitions ||= declaration.definitions.to_a
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def same_location?(location, range)
|
|
80
|
+
same_file?(location) && RuboCop::Callbacksystems::ProjectIndex::Location.for_rubydex(location) ==
|
|
81
|
+
RuboCop::Callbacksystems::ProjectIndex::Location.for_ast(range, uri: location.uri)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def same_file?(location)
|
|
85
|
+
File.identical?(location.to_file_path, file)
|
|
86
|
+
rescue SystemCallError, Rubydex::Location::NotFileUriError
|
|
87
|
+
false
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# A definition beneath a placeholder namespace can be real, but Rubydex cannot prove what an autoloader may
|
|
91
|
+
# already have installed there. Every owner up to Object therefore needs a declaration of its own.
|
|
92
|
+
def owners_known?
|
|
93
|
+
owner_chain_known_from?(declaration.owner)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def owner_chain_known_from?(owner)
|
|
97
|
+
owner.name == "Object" || (owner.definitions.any? && owner_chain_known_from?(owner.owner))
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def later_in_current_file?(definition)
|
|
101
|
+
same_file?(definition.location) && after_current_definition?(definition.location)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def after_current_definition?(location)
|
|
105
|
+
([ location.start_line, location.start_column ] <=> current_position).positive?
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def current_position
|
|
109
|
+
@current_position ||= current_definition.location.then { [ it.start_line, it.start_column ] }
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
class EmptyNestedClass
|
|
114
|
+
include RuboCop::Callbacksystems::Helpers
|
|
115
|
+
|
|
116
|
+
MESSAGE = "Declare `%<name>s` as `Class.new` instead of a class definition with no body."
|
|
117
|
+
|
|
118
|
+
def initialize(node, source_comments, assignment_safe:)
|
|
119
|
+
@node = node
|
|
120
|
+
@source_comments = source_comments
|
|
121
|
+
@assignment_safe = assignment_safe
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def offense
|
|
125
|
+
if node.body.nil? && nested?
|
|
126
|
+
RuboCop::Callbacksystems::Offense.new(node, message, correcting: correctable?) { correct(it) }
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
private
|
|
131
|
+
attr_reader :node, :source_comments, :assignment_safe
|
|
132
|
+
|
|
133
|
+
def nested?
|
|
134
|
+
enclosing_class_or_module_of(node).present?
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def message
|
|
138
|
+
format(MESSAGE, name: class_name_of(node))
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def correctable?
|
|
142
|
+
assignment_safe && expression_result.discarded? && superclass_without_heredoc? &&
|
|
143
|
+
comments.none? { tooling_comment?(it) }
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def expression_result
|
|
147
|
+
@expression_result ||= RuboCop::Callbacksystems::Execution::DiscardedExpression.new(node)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def superclass_without_heredoc?
|
|
151
|
+
node.parent_class.nil? || !carries_heredoc?(node.parent_class)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def comments
|
|
155
|
+
@comments ||= [ *source_comments.within(node), closing_comment ]
|
|
156
|
+
.compact.uniq.sort_by { it.source_range.begin_pos }
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def closing_comment
|
|
160
|
+
@closing_comment ||= source_comments.trailing_comment_on(node.last_line)&.then do |comment|
|
|
161
|
+
comment if directly_trails_definition?(comment)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def directly_trails_definition?(comment)
|
|
166
|
+
node.source_range.with(begin_pos: node.source_range.end_pos, end_pos: comment.source_range.begin_pos)
|
|
167
|
+
.source.match?(/\A[ \t]*;?[ \t]*\z/)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def correct(corrector)
|
|
171
|
+
corrector.replace(correction_range, corrected_source)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def correction_range
|
|
175
|
+
closing_comment ? node.source_range.join(closing_comment.source_range) : node.source_range
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def corrected_source
|
|
179
|
+
CommentedDeclaration.new(declaration, movable_comments, indentation:).source
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def declaration
|
|
183
|
+
"#{class_name_of(node)} = Class.new#{inherited_class}"
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def inherited_class
|
|
187
|
+
"(#{node.parent_class.source})" if node.parent_class
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def movable_comments
|
|
191
|
+
comments.reject { superclass_contains?(it) }
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def superclass_contains?(comment)
|
|
195
|
+
node.parent_class&.source_range&.contains?(comment.source_range)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def indentation
|
|
199
|
+
preceding = node.source_range.source_line[0...node.source_range.column]
|
|
200
|
+
|
|
201
|
+
preceding.strip.empty? ? preceding : " " * node.source_range.column
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Comments from the definition keep their order around the declaration, with one former trailing comment still
|
|
205
|
+
# trailing code and every other comment standing on its own line.
|
|
206
|
+
class CommentedDeclaration
|
|
207
|
+
include RuboCop::Callbacksystems::Helpers
|
|
208
|
+
|
|
209
|
+
def initialize(declaration, comments, indentation:)
|
|
210
|
+
@declaration = declaration
|
|
211
|
+
@comments = comments
|
|
212
|
+
@indentation = indentation
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def source
|
|
216
|
+
[ *before, declaration_with_comment, *after ].join("\n#{indentation}")
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
private
|
|
220
|
+
attr_reader :declaration, :comments, :indentation
|
|
221
|
+
|
|
222
|
+
def before
|
|
223
|
+
comments.take(anchor_position).map(&:text)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def anchor_position
|
|
227
|
+
@anchor_position ||= comments.index { !own_line_comment?(it) } || comments.size
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def declaration_with_comment
|
|
231
|
+
[ declaration, anchor&.text ].compact.join(" ")
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def anchor
|
|
235
|
+
comments[anchor_position] unless anchor_position == comments.size
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def after
|
|
239
|
+
comments.drop(anchor_position + anchor_count).map(&:text)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def anchor_count
|
|
243
|
+
anchor ? 1 : 0
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# A `Data.define`, `Struct.new` or `Class.new` given a block defines a class
|
|
2
|
+
# with behavior, so it reads better as the class it is.
|
|
3
|
+
#
|
|
4
|
+
# The block form also changes what the code inside it means. A constant
|
|
5
|
+
# declared there lands in the enclosing namespace instead of the new class,
|
|
6
|
+
# `Module.nesting` never mentions the class, and the rules that govern a class
|
|
7
|
+
# body do not reach inside a block.
|
|
8
|
+
#
|
|
9
|
+
# A builder with no block only lists its members and stays as it is.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# # bad
|
|
13
|
+
# Route = Data.define(:host) do
|
|
14
|
+
# def matches?(other)
|
|
15
|
+
# host == other
|
|
16
|
+
# end
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# # good
|
|
20
|
+
# class Route < Data.define(:host)
|
|
21
|
+
# def matches?(other)
|
|
22
|
+
# host == other
|
|
23
|
+
# end
|
|
24
|
+
# end
|
|
25
|
+
#
|
|
26
|
+
# # good - nothing but its members
|
|
27
|
+
# Entry = Data.define(:sku, :quantity)
|
|
28
|
+
#
|
|
29
|
+
class RuboCop::Cop::Callbacksystems::PreferClassOverBuilderBlock < RuboCop::Cop::Callbacksystems::Base
|
|
30
|
+
extend RuboCop::Cop::AutoCorrector
|
|
31
|
+
|
|
32
|
+
def on_casgn(node)
|
|
33
|
+
report BuilderBlock.new(node)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
class BuilderBlock
|
|
38
|
+
include RuboCop::Callbacksystems::Helpers
|
|
39
|
+
|
|
40
|
+
MESSAGE = "Define `%<name>s` as a class instead of a block given to `%<builder>s`."
|
|
41
|
+
|
|
42
|
+
def initialize(node)
|
|
43
|
+
@node = node
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def offense
|
|
47
|
+
if class_with_body?(node)
|
|
48
|
+
RuboCop::Callbacksystems::Offense.new(node, message, correcting: rewritable?) { correct(it) }
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
attr_reader :node
|
|
54
|
+
|
|
55
|
+
def message
|
|
56
|
+
format(MESSAGE, name: assigned_name, builder: builder_name)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def assigned_name
|
|
60
|
+
node.source_range.with(end_pos: node.loc.operator.begin_pos).source.rstrip
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def builder_name
|
|
64
|
+
"#{builder_call.receiver.source}.#{builder_call.method_name}"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def builder_call
|
|
68
|
+
block.send_node
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def block
|
|
72
|
+
node.expression
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Only `do ... end` is rewritten, since anything else needs the body moved onto its own lines to stay valid.
|
|
76
|
+
def rewritable?
|
|
77
|
+
block.multiline? && block.loc.begin.source == "do" && block.argument_list.empty? &&
|
|
78
|
+
!captures_local_variable? && !reads_or_assigns_constant? && !reads_or_assigns_class_variable? &&
|
|
79
|
+
!builder_control_flow?
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def captures_local_variable?
|
|
83
|
+
nodes_in(block.body, :lvar).any? do |read|
|
|
84
|
+
read.each_ancestor(:any_def).none? &&
|
|
85
|
+
!name_rebound_between?(read.name, node: read, boundary: block)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def reads_or_assigns_constant?
|
|
90
|
+
nodes_in(block.body, :const, :casgn).any?
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def reads_or_assigns_class_variable?
|
|
94
|
+
nodes_in(block.body, :cvar, :cvasgn).any?
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def builder_control_flow?
|
|
98
|
+
nodes_in(block.body, :break, :next, :redo).any? do |flow|
|
|
99
|
+
flow.each_ancestor(*BLOCK_NODE_TYPES).first.equal?(block)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def correct(corrector)
|
|
104
|
+
corrector.replace(header, "class #{assigned_name}#{inheritance}")
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def header
|
|
108
|
+
node.source_range.with(end_pos: block.loc.begin.end_pos)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# `Class.new(Base)` is a plain subclass of `Base`, while `Data.define` and `Struct.new` build the superclass.
|
|
112
|
+
def inheritance
|
|
113
|
+
plain_subclass? ? argument_inheritance : " < #{builder_call.source}"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def plain_subclass?
|
|
117
|
+
builder_call.receiver.short_name == :Class
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def argument_inheritance
|
|
121
|
+
" < #{builder_call.first_argument.source}" if builder_call.first_argument
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# Detects a parameter given its fallback value at the top of the method. A
|
|
2
|
+
# `||=` or a `nil?` guard opening the body makes the reader work out what the
|
|
3
|
+
# parameter really is, where a default in the signature says it right where the
|
|
4
|
+
# parameter is declared, so the body starts with the work.
|
|
5
|
+
#
|
|
6
|
+
# The cop recommends the change for a positional parameter and a keyword
|
|
7
|
+
# declared as `name: nil`. It leaves a required keyword alone, since a default
|
|
8
|
+
# would change what callers must pass, as well as a fallback reading a parameter
|
|
9
|
+
# declared to its right, which a default cannot see. Only the run of fallbacks
|
|
10
|
+
# opening the body counts: one that follows other statements may depend on them.
|
|
11
|
+
#
|
|
12
|
+
# The offense has no automatic correction. A default only runs when an argument
|
|
13
|
+
# is omitted, while these fallbacks also run for an explicit `nil` or `false`;
|
|
14
|
+
# making a required positional parameter optional changes its observable arity
|
|
15
|
+
# too. Choosing the new calling contract belongs to the author.
|
|
16
|
+
#
|
|
17
|
+
# @example
|
|
18
|
+
# # bad
|
|
19
|
+
# def deliver(recipients)
|
|
20
|
+
# recipients ||= [ owner ]
|
|
21
|
+
# recipients.each { mail(it) }
|
|
22
|
+
# end
|
|
23
|
+
#
|
|
24
|
+
# # bad
|
|
25
|
+
# def deliver(recipients: nil)
|
|
26
|
+
# recipients = [ owner ] if recipients.nil?
|
|
27
|
+
# recipients.each { mail(it) }
|
|
28
|
+
# end
|
|
29
|
+
#
|
|
30
|
+
# # good
|
|
31
|
+
# def deliver(recipients = [ owner ])
|
|
32
|
+
# recipients.each { mail(it) }
|
|
33
|
+
# end
|
|
34
|
+
#
|
|
35
|
+
# # good
|
|
36
|
+
# def deliver(recipients: [ owner ])
|
|
37
|
+
# recipients.each { mail(it) }
|
|
38
|
+
# end
|
|
39
|
+
#
|
|
40
|
+
# # good - a default cannot read a parameter to its right
|
|
41
|
+
# def deliver(recipients, sender)
|
|
42
|
+
# recipients ||= [ sender ]
|
|
43
|
+
# recipients.each { mail(it) }
|
|
44
|
+
# end
|
|
45
|
+
#
|
|
46
|
+
class RuboCop::Cop::Callbacksystems::PreferDefaultArgument < RuboCop::Cop::Callbacksystems::Base
|
|
47
|
+
def on_def(node)
|
|
48
|
+
report_each LeadingFallbacks.new(node)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
alias on_defs on_def
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
# The fallbacks opening a method body, one per parameter, narrowed to those expressible as a signature default.
|
|
55
|
+
class LeadingFallbacks
|
|
56
|
+
include RuboCop::Callbacksystems::Helpers
|
|
57
|
+
|
|
58
|
+
def initialize(method_node)
|
|
59
|
+
@method_node = method_node
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def each_offense
|
|
63
|
+
reportable.each { yield it.offense } if followed_by_work?
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
attr_reader :method_node
|
|
68
|
+
|
|
69
|
+
def followed_by_work?
|
|
70
|
+
statements.size > leading.size
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def statements
|
|
74
|
+
statements_in(method_node.body)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def leading
|
|
78
|
+
@leading ||= statements.map { Fallback.new(it, method_node) }.take_while(&:fallback?).uniq(&:name)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Ruby allows one group of optional positional parameters, so a recommendation must not open a second one.
|
|
82
|
+
def reportable
|
|
83
|
+
optional_group_stays_contiguous? ? candidates : candidates.select(&:keyword?)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def optional_group_stays_contiguous?
|
|
87
|
+
optional_runs.size <= 1
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def optional_runs
|
|
91
|
+
positional_parameters
|
|
92
|
+
.chunk_while { |left, right| optional_with_candidate?(left) == optional_with_candidate?(right) }
|
|
93
|
+
.select { optional_with_candidate?(it.first) }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def positional_parameters
|
|
97
|
+
method_node.arguments.select { it.type?(:arg, :optarg) }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def optional_with_candidate?(parameter)
|
|
101
|
+
parameter.optarg_type? || candidates.any? { it.parameter.equal?(parameter) }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def candidates
|
|
105
|
+
@candidates ||= leading.select(&:reportable?)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
class Fallback
|
|
110
|
+
include RuboCop::Callbacksystems::Helpers
|
|
111
|
+
extend RuboCop::AST::NodePattern::Macros
|
|
112
|
+
|
|
113
|
+
MESSAGE = "Give `%<name>s` its default in the signature instead of a fallback in the body."
|
|
114
|
+
|
|
115
|
+
# @!method fallback_of(node)
|
|
116
|
+
def_node_matcher :fallback_of, <<~PATTERN
|
|
117
|
+
{
|
|
118
|
+
(or_asgn (lvasgn $_name) $_value)
|
|
119
|
+
(if (send (lvar $_name) :nil?) (lvasgn _name $_value) nil?)
|
|
120
|
+
(if (lvar $_name) nil? (lvasgn _name $_value))
|
|
121
|
+
}
|
|
122
|
+
PATTERN
|
|
123
|
+
|
|
124
|
+
def initialize(statement, method_node)
|
|
125
|
+
@statement = statement
|
|
126
|
+
@method_node = method_node
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def fallback?
|
|
130
|
+
parameter.present?
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def parameter
|
|
134
|
+
@parameter ||= named_parameters.find { it.name == name } if name
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def name
|
|
138
|
+
captures&.first
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def keyword?
|
|
142
|
+
parameter.kwoptarg_type?
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def reportable?
|
|
146
|
+
defaultable? && value.single_line? && !carries_heredoc?(value) && !reads_parameter_from_itself_on? &&
|
|
147
|
+
alone_on_its_lines?
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def offense
|
|
151
|
+
RuboCop::Callbacksystems::Offense.new(statement, format(MESSAGE, name:))
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
private
|
|
155
|
+
attr_reader :statement, :method_node
|
|
156
|
+
|
|
157
|
+
def captures
|
|
158
|
+
@captures ||= fallback_of(statement)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def named_parameters
|
|
162
|
+
method_node.arguments.reject(&:mlhs_type?)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def defaultable?
|
|
166
|
+
(parameter.arg_type? && !positional_parameter_after_rest?) ||
|
|
167
|
+
(keyword? && parameter.default_value.nil_type?)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def positional_parameter_after_rest?
|
|
171
|
+
method_node.arguments.take_while { !it.equal?(parameter) }.any?(&:restarg_type?)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def value
|
|
175
|
+
captures.second
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def reads_parameter_from_itself_on?
|
|
179
|
+
nodes_in(value, :lvar).any? do |read|
|
|
180
|
+
parameter_names_from_itself_on.include?(read.name) &&
|
|
181
|
+
!name_rebound_between?(read.name, node: read, boundary: method_node)
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def parameter_names_from_itself_on
|
|
186
|
+
method_node.arguments.drop_while { !it.equal?(parameter) }.flat_map { ParameterNames.new(it).to_a }
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def alone_on_its_lines?
|
|
190
|
+
statement.first_line > method_node.first_line && statement.last_line < next_code_line
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def next_code_line
|
|
194
|
+
statement.right_sibling.first_line
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Names introduced by one argument, including the leaves of a destructured positional argument.
|
|
199
|
+
class ParameterNames
|
|
200
|
+
include Enumerable
|
|
201
|
+
|
|
202
|
+
def initialize(parameter)
|
|
203
|
+
@parameter = parameter
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def each
|
|
207
|
+
if block_given?
|
|
208
|
+
pending = [ parameter ]
|
|
209
|
+
until pending.empty?
|
|
210
|
+
current = pending.pop
|
|
211
|
+
if current.mlhs_type?
|
|
212
|
+
pending.concat(current.each_child_node.to_a.reverse)
|
|
213
|
+
elsif current.respond_to?(:name) && current.name
|
|
214
|
+
yield current.name
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
else
|
|
218
|
+
to_enum(__method__)
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
private
|
|
223
|
+
attr_reader :parameter
|
|
224
|
+
end
|
|
225
|
+
end
|