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,478 @@
|
|
|
1
|
+
# A class body reads in one order: the mixins and the class attribute assignments that say what it is made of, then
|
|
2
|
+
# the values it holds, then the classes it declares in a line, then the `attr_reader`, `attribute` and
|
|
3
|
+
# `class_attribute` that hold its state, then the associations it stands in, then the `delegate` that reaches through
|
|
4
|
+
# them, then the macros that declare its behavior, then the methods, and last the nested classes with a body of their
|
|
5
|
+
# own.
|
|
6
|
+
#
|
|
7
|
+
# A constant tells values from classes by the case of its name rather than by what built it: `Naming/ConstantName` asks
|
|
8
|
+
# for SCREAMING_SNAKE_CASE on a plain value and only lets a name off when the value comes from a call, which is how a
|
|
9
|
+
# class gets built. So `COLUMNS` reads as a value and `Row` as a class, whether it came from `Data.define`, `Struct.new`
|
|
10
|
+
# or a method of your own.
|
|
11
|
+
#
|
|
12
|
+
# A method is never the one at fault. `MethodInvocationOrder` owns the order of those, so a nested class written above
|
|
13
|
+
# the methods is reported once, on the class, rather than once per method it sits over.
|
|
14
|
+
#
|
|
15
|
+
# Nothing moves over a name it reads. A value built from a nested class, or a mixin naming a module declared there, is
|
|
16
|
+
# read while the body loads, so the two keep the order they were written in.
|
|
17
|
+
#
|
|
18
|
+
# `Layout/ClassStructure` lists no category for constants, since putting one there would forbid declaring them in the
|
|
19
|
+
# private section, and it lets an `attr_reader` sit below a method, so the whole order lives here instead and that cop
|
|
20
|
+
# ships off. Leaving both on has them rewrite the same statement in one pass, which breaks the file.
|
|
21
|
+
#
|
|
22
|
+
# @example
|
|
23
|
+
# # bad - the constant sits below the method that reads it
|
|
24
|
+
# class Report
|
|
25
|
+
# def total
|
|
26
|
+
# COLUMNS.size
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
# COLUMNS = [ :name, :amount ]
|
|
30
|
+
# end
|
|
31
|
+
#
|
|
32
|
+
# # good
|
|
33
|
+
# class Report
|
|
34
|
+
# COLUMNS = [ :name, :amount ]
|
|
35
|
+
#
|
|
36
|
+
# def total
|
|
37
|
+
# COLUMNS.size
|
|
38
|
+
# end
|
|
39
|
+
# end
|
|
40
|
+
#
|
|
41
|
+
# # bad - the reader comes before the constant, and the class before both
|
|
42
|
+
# class Report
|
|
43
|
+
# Row = Data.define(:name)
|
|
44
|
+
# attr_reader :rows
|
|
45
|
+
# COLUMNS = [ :name ]
|
|
46
|
+
# end
|
|
47
|
+
#
|
|
48
|
+
# # good
|
|
49
|
+
# class Report
|
|
50
|
+
# COLUMNS = [ :name ]
|
|
51
|
+
# Row = Data.define(:name)
|
|
52
|
+
# attr_reader :rows
|
|
53
|
+
# end
|
|
54
|
+
#
|
|
55
|
+
# # bad - the nested class sits above the methods
|
|
56
|
+
# class Report
|
|
57
|
+
# class Row
|
|
58
|
+
# def to_s = name
|
|
59
|
+
# end
|
|
60
|
+
#
|
|
61
|
+
# def rows
|
|
62
|
+
# [ Row.new ]
|
|
63
|
+
# end
|
|
64
|
+
# end
|
|
65
|
+
#
|
|
66
|
+
# # good
|
|
67
|
+
# class Report
|
|
68
|
+
# def rows
|
|
69
|
+
# [ Row.new ]
|
|
70
|
+
# end
|
|
71
|
+
#
|
|
72
|
+
# class Row
|
|
73
|
+
# def to_s = name
|
|
74
|
+
# end
|
|
75
|
+
# end
|
|
76
|
+
#
|
|
77
|
+
class RuboCop::Cop::Callbacksystems::BodyOrder < RuboCop::Cop::Callbacksystems::Base
|
|
78
|
+
extend RuboCop::Cop::AutoCorrector
|
|
79
|
+
|
|
80
|
+
def on_new_investigation
|
|
81
|
+
report_each Bodies.new(processed_source)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
class Bodies
|
|
86
|
+
include RuboCop::Callbacksystems::Helpers
|
|
87
|
+
|
|
88
|
+
def initialize(processed_source)
|
|
89
|
+
@processed_source = processed_source
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def each_offense(&block)
|
|
93
|
+
lifts.each { it.each_offense(&block) }
|
|
94
|
+
sinks.each { it.each_offense(&block) }
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
attr_reader :processed_source
|
|
99
|
+
|
|
100
|
+
def lifts
|
|
101
|
+
@lifts ||= sections.map { Lifts.new(it) }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def sections
|
|
105
|
+
@sections ||= bodies.flat_map { sections_of(it) }
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def bodies
|
|
109
|
+
nodes_in(processed_source.ast, :class, :module, :sclass)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def sections_of(node)
|
|
113
|
+
statements_in(node.body)
|
|
114
|
+
.slice_when { |_, right| visibility_modifier_of(right) }
|
|
115
|
+
.map { Entries.new(it.reject { visibility_modifier_of(it) }, processed_source) }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def sinks
|
|
119
|
+
@sinks ||= sections.map { Sinks.new(it, permit: permit) }
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# A lift rewrites the span the classes sit in, and a class carries everything inside it, so one per pass.
|
|
123
|
+
def permit
|
|
124
|
+
@permit ||= RuboCop::Callbacksystems::Autocorrection::RewritePermit.new(granted: lifts.none?(&:any?))
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
class Entries
|
|
129
|
+
include Enumerable
|
|
130
|
+
|
|
131
|
+
delegate :each, to: :statements
|
|
132
|
+
|
|
133
|
+
def initialize(nodes, processed_source)
|
|
134
|
+
@nodes = nodes
|
|
135
|
+
@processed_source = processed_source
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def blocker_for(declaration)
|
|
139
|
+
take_while { it.begin_position < declaration.begin_position }.find { it.rank > declaration.rank }
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def methods_below(nested_class)
|
|
143
|
+
select { it.method? && it.begin_position > nested_class.begin_position }
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def declarations
|
|
147
|
+
select(&:liftable?)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def method_precedence_kept?
|
|
151
|
+
reordered_pairs.all?(&:precedence_kept?)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# A comment unattached to either statement divides the body into semantic sections.
|
|
155
|
+
def uninterrupted?
|
|
156
|
+
each_cons(2).none? { |above, below| above.range.end.join(below.range.begin).source.strip.present? }
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def tooling_scope_kept?
|
|
160
|
+
none?(&:contains_tooling_comment?)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def anchor
|
|
164
|
+
find(&:body?)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
private
|
|
168
|
+
attr_reader :nodes, :processed_source
|
|
169
|
+
|
|
170
|
+
def statements
|
|
171
|
+
@statements ||= nodes.map { Statement.new(it, nodes, processed_source) }
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def reordered_pairs
|
|
175
|
+
each_with_index.flat_map do |first, index|
|
|
176
|
+
drop(index.next).map { ReorderedPair.new(first, it) }.select(&:order_changes?)
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
class ReorderedPair < Data.define(:first, :second)
|
|
181
|
+
def order_changes?
|
|
182
|
+
second.liftable? && (!first.liftable? || first.rank > second.rank)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def precedence_kept?
|
|
186
|
+
if first_names&.empty? || second_names&.empty?
|
|
187
|
+
true
|
|
188
|
+
else
|
|
189
|
+
first_names && second_names && !first_names.intersect?(second_names)
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
private
|
|
194
|
+
def first_names
|
|
195
|
+
first.defined_method_names
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def second_names
|
|
199
|
+
second.defined_method_names
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Statements out of place, whose first offense carries the rewrite for the whole group.
|
|
205
|
+
class Misplacements
|
|
206
|
+
include Enumerable
|
|
207
|
+
|
|
208
|
+
ORDER = "a body reads what it holds, then what it does, then its nested classes."
|
|
209
|
+
|
|
210
|
+
delegate :each, to: :misplaced
|
|
211
|
+
|
|
212
|
+
def initialize(entries)
|
|
213
|
+
@entries = entries
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def each_offense
|
|
217
|
+
rewriting = correctable?
|
|
218
|
+
each_with_index do |statement, index|
|
|
219
|
+
yield RuboCop::Callbacksystems::Offense.new(statement.node, message_for(statement),
|
|
220
|
+
correcting: rewriting && index.zero?) { rewrite(it) }
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
private
|
|
225
|
+
attr_reader :entries
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
class Lifts < Misplacements
|
|
229
|
+
MESSAGE = "Move `%<name>s` above `%<blocker>s`; #{ORDER}"
|
|
230
|
+
|
|
231
|
+
private
|
|
232
|
+
def misplaced
|
|
233
|
+
@misplaced ||= entries.select { it.liftable? && entries.blocker_for(it) && it.movable? }
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# The rewrite spans every declaration at once, so one that cannot move would be written over.
|
|
237
|
+
def correctable?
|
|
238
|
+
entries.uninterrupted? && entries.tooling_scope_kept? && entries.declarations.all?(&:movable?) &&
|
|
239
|
+
entries.method_precedence_kept?
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def message_for(declaration)
|
|
243
|
+
format(MESSAGE, name: declaration.name, blocker: entries.blocker_for(declaration).opening_line)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def rewrite(corrector)
|
|
247
|
+
Reordering.new(entries.declarations, entries.anchor, corrector).rewrite
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# The nested classes sitting above what the body does, which `MethodInvocationOrder` never moves.
|
|
252
|
+
class Sinks < Misplacements
|
|
253
|
+
MESSAGE = "Move `%<name>s` below `%<blocked>s`; #{ORDER}"
|
|
254
|
+
|
|
255
|
+
def initialize(entries, permit:)
|
|
256
|
+
super(entries)
|
|
257
|
+
@permit = permit
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
private
|
|
261
|
+
attr_reader :permit
|
|
262
|
+
|
|
263
|
+
def correctable?
|
|
264
|
+
any? && entries.uninterrupted? && entries.tooling_scope_kept? && permit.claim
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def message_for(nested_class)
|
|
268
|
+
format(MESSAGE, name: nested_class.name, blocked: entries.methods_below(nested_class).first.opening_line)
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def rewrite(corrector)
|
|
272
|
+
Sinking.new(misplaced, entries.to_a.last, corrector).rewrite
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def misplaced
|
|
276
|
+
@misplaced ||= entries.select { it.nested_class? && entries.methods_below(it).any? && sinkable?(it) }
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# A declaration naming the class reads it while the body loads.
|
|
280
|
+
def sinkable?(nested_class)
|
|
281
|
+
entries.declarations.none? { it.reads?(nested_class.declared_name) }
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
class Reordering
|
|
286
|
+
include RuboCop::Callbacksystems::Helpers
|
|
287
|
+
|
|
288
|
+
def initialize(declarations, anchor, corrector)
|
|
289
|
+
@declarations = declarations
|
|
290
|
+
@anchor = anchor
|
|
291
|
+
@corrector = corrector
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def rewrite
|
|
295
|
+
leading.any? ? replace_leading_span : insert_above_anchor
|
|
296
|
+
statement_removal_ranges_of(trailing.map(&:range)).each { corrector.remove(it) }
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
private
|
|
300
|
+
attr_reader :declarations, :anchor, :corrector
|
|
301
|
+
|
|
302
|
+
def leading
|
|
303
|
+
anchor ? declarations.reject { below_anchor?(it) } : declarations
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def below_anchor?(declaration)
|
|
307
|
+
declaration.begin_position > anchor.begin_position
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def replace_leading_span
|
|
311
|
+
corrector.replace(span_of(leading.map(&:range)), ordered_source)
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def ordered_source
|
|
315
|
+
ordered.map(&:source).zip(separators).join
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def ordered
|
|
319
|
+
@ordered ||= declarations.sort_by.with_index { |declaration, index| [ declaration.rank, index ] }
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def separators
|
|
323
|
+
ordered.each_cons(2).map { |above, below| Neighbors.new(above:, below:, declarations:).separator } + [ "" ]
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def insert_above_anchor
|
|
327
|
+
corrector.insert_before(anchor.range, "#{ordered_source}\n\n")
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def trailing
|
|
331
|
+
declarations - leading
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
# Two declarations landing next to each other, kept apart only by a blank line the author wrote between them.
|
|
336
|
+
class Neighbors
|
|
337
|
+
BLANK_LINE = /\n[ \t]*\n/
|
|
338
|
+
|
|
339
|
+
def initialize(above:, below:, declarations:)
|
|
340
|
+
@above = above
|
|
341
|
+
@below = below
|
|
342
|
+
@declarations = declarations
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def separator
|
|
346
|
+
blank_written? ? "\n\n" : "\n"
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
private
|
|
350
|
+
attr_reader :above, :below, :declarations
|
|
351
|
+
|
|
352
|
+
def blank_written?
|
|
353
|
+
declarations.each_cons(2).any? do |first, second|
|
|
354
|
+
written_span.cover?(first.begin_position) && blank_line_between?(first, second)
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def written_span
|
|
359
|
+
earlier, later = [ above, below ].map(&:begin_position).sort
|
|
360
|
+
earlier...later
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def blank_line_between?(first, second)
|
|
364
|
+
first.range.end.join(second.range.begin).source.match?(BLANK_LINE)
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
class Sinking
|
|
369
|
+
include RuboCop::Callbacksystems::Helpers
|
|
370
|
+
|
|
371
|
+
def initialize(nested_classes, tail, corrector)
|
|
372
|
+
@nested_classes = nested_classes
|
|
373
|
+
@tail = tail
|
|
374
|
+
@corrector = corrector
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def rewrite
|
|
378
|
+
corrector.insert_after(tail.range, "\n\n#{ordered_source}")
|
|
379
|
+
statement_removal_ranges_of(nested_classes.map(&:range)).each { corrector.remove(it) }
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
private
|
|
383
|
+
attr_reader :nested_classes, :tail, :corrector
|
|
384
|
+
|
|
385
|
+
def ordered_source
|
|
386
|
+
nested_classes.map(&:source).join("\n\n")
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
class Statement
|
|
391
|
+
include RuboCop::Callbacksystems::Helpers
|
|
392
|
+
|
|
393
|
+
attr_reader :node
|
|
394
|
+
|
|
395
|
+
delegate :range, :source, :contains_tooling_comment?, to: :block
|
|
396
|
+
|
|
397
|
+
def initialize(node, statements, processed_source)
|
|
398
|
+
@node = node
|
|
399
|
+
@statements = statements
|
|
400
|
+
@processed_source = processed_source
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
def liftable?
|
|
404
|
+
rank <= RuboCop::Callbacksystems::ClassStructure::StatementRank::MACRO
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
def rank
|
|
408
|
+
@rank ||= RuboCop::Callbacksystems::ClassStructure::StatementRank.new(node, statements).to_i
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
def method?
|
|
412
|
+
rank == RuboCop::Callbacksystems::ClassStructure::StatementRank::METHOD
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
def nested_class?
|
|
416
|
+
rank == RuboCop::Callbacksystems::ClassStructure::StatementRank::NESTED_CLASS
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
def body?
|
|
420
|
+
rank >= RuboCop::Callbacksystems::ClassStructure::StatementRank::METHOD
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
# Only a read declaration of a higher rank would sort above this one, so any other moves along with it.
|
|
424
|
+
def movable?
|
|
425
|
+
declarations_read_before.none? { it.rank > rank }
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
def reads?(constant_name)
|
|
429
|
+
constant_name && names_read.include?(constant_name)
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
def declared_name
|
|
433
|
+
declared_name_of(node)
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
def begin_position
|
|
437
|
+
node.source_range.begin_pos
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
def name
|
|
441
|
+
node.casgn_type? ? node.name : opening_line
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
def opening_line
|
|
445
|
+
node.source.lines.first.strip
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
def defined_method_names
|
|
449
|
+
@defined_method_names ||= RuboCop::Callbacksystems::Methods::StatementDefinitions.new(node).names
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
private
|
|
453
|
+
attr_reader :statements, :processed_source
|
|
454
|
+
|
|
455
|
+
def block
|
|
456
|
+
@block ||= RuboCop::Callbacksystems::Source::StatementWithComments.new(node, processed_source)
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
def declarations_read_before
|
|
460
|
+
statements.take_while { !it.equal?(node) }
|
|
461
|
+
.select { names_read.include?(declared_name_of(it)) }
|
|
462
|
+
.map { self.class.new(it, statements, processed_source) }
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
# A `private_constant :X` reads the `X` it hides.
|
|
466
|
+
def names_read
|
|
467
|
+
@names_read ||= node.each_descendant(:const, :lvar, :send).filter_map { read_name_of(it) }.to_set
|
|
468
|
+
.merge(private_constant_names_of(node))
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
def read_name_of(descendant)
|
|
472
|
+
if descendant.const_type? then descendant.short_name
|
|
473
|
+
elsif descendant.lvar_type? then descendant.name
|
|
474
|
+
elsif bare_send?(descendant) && descendant.arguments.empty? then descendant.method_name
|
|
475
|
+
end
|
|
476
|
+
end
|
|
477
|
+
end
|
|
478
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Requires parentheses on builder method calls (`.new`, ActiveRecord persistence,
|
|
2
|
+
# finders, strong parameters) when they have arguments. A call that builds or
|
|
3
|
+
# looks up a record is an expression handing back a value, and the parentheses
|
|
4
|
+
# mark it as one, while a bare call reads as a declaration the way a macro does
|
|
5
|
+
# (`validates :name`). Bang methods and backslash continuations are exempt.
|
|
6
|
+
#
|
|
7
|
+
# @example
|
|
8
|
+
# # bad
|
|
9
|
+
# User.new name: "John"
|
|
10
|
+
# User.create name: "John", email: "j@example.com"
|
|
11
|
+
# User.find_by name: "John"
|
|
12
|
+
# params.permit :name, :email
|
|
13
|
+
#
|
|
14
|
+
# # good
|
|
15
|
+
# User.new(name: "John")
|
|
16
|
+
# User.create(name: "John", email: "j@example.com")
|
|
17
|
+
# User.find_by(name: "John")
|
|
18
|
+
# params.permit(:name, :email)
|
|
19
|
+
#
|
|
20
|
+
# # good - bang methods exempt
|
|
21
|
+
# User.create! name: "John"
|
|
22
|
+
#
|
|
23
|
+
# # good - backslash continuation
|
|
24
|
+
# User.create \
|
|
25
|
+
# name: "John"
|
|
26
|
+
#
|
|
27
|
+
class RuboCop::Cop::Callbacksystems::BuilderMethodParentheses < RuboCop::Cop::Callbacksystems::Base
|
|
28
|
+
extend RuboCop::Cop::AutoCorrector
|
|
29
|
+
|
|
30
|
+
def on_send(node)
|
|
31
|
+
report BuilderCall.new(node) if builder_method?(node)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
alias on_csend on_send
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
class BuilderCall
|
|
38
|
+
MESSAGE = "Use parentheses for `%<method>s` when passing arguments."
|
|
39
|
+
|
|
40
|
+
def initialize(node)
|
|
41
|
+
@node = node
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def offense
|
|
45
|
+
RuboCop::Callbacksystems::Offense.new(node, message) { correct(it) } if missing_parentheses?
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
attr_reader :node
|
|
50
|
+
|
|
51
|
+
def missing_parentheses?
|
|
52
|
+
node.arguments? && !node.parenthesized? && !backslash_continuation?
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def backslash_continuation?
|
|
56
|
+
gap_range.source.include?("\\")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def gap_range
|
|
60
|
+
Parser::Source::Range.new \
|
|
61
|
+
node.source_range.source_buffer, node.loc.selector.end_pos,
|
|
62
|
+
node.first_argument.source_range.begin_pos
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def message
|
|
66
|
+
format(MESSAGE, method: node.method_name)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def correct(corrector)
|
|
70
|
+
corrector.replace(gap_range, "(")
|
|
71
|
+
corrector.insert_after(node.last_argument, ")")
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# Detects multiline expressions that can fit on a single line and collapses them.
|
|
2
|
+
# Applies to hash literals, array literals, and method calls with backslash
|
|
3
|
+
# continuation or parenthesized arguments. An expression spread over several
|
|
4
|
+
# lines when one would hold it costs the reader lines for nothing and hides how
|
|
5
|
+
# short the expression is.
|
|
6
|
+
#
|
|
7
|
+
# @example
|
|
8
|
+
# # bad - hash that fits on one line
|
|
9
|
+
# x = {
|
|
10
|
+
# foo: 1,
|
|
11
|
+
# bar: 2
|
|
12
|
+
# }
|
|
13
|
+
#
|
|
14
|
+
# # good
|
|
15
|
+
# x = { foo: 1, bar: 2 }
|
|
16
|
+
#
|
|
17
|
+
# # bad - array that fits on one line
|
|
18
|
+
# x = [
|
|
19
|
+
# 1,
|
|
20
|
+
# 2,
|
|
21
|
+
# 3
|
|
22
|
+
# ]
|
|
23
|
+
#
|
|
24
|
+
# # good
|
|
25
|
+
# x = [ 1, 2, 3 ]
|
|
26
|
+
#
|
|
27
|
+
# # bad - backslash continuation that fits on one line
|
|
28
|
+
# redirect_to \
|
|
29
|
+
# users_path, notice: "Done"
|
|
30
|
+
#
|
|
31
|
+
# # good
|
|
32
|
+
# redirect_to users_path, notice: "Done"
|
|
33
|
+
#
|
|
34
|
+
# # bad - parenthesized call that fits on one line
|
|
35
|
+
# User.new(
|
|
36
|
+
# name: "John"
|
|
37
|
+
# )
|
|
38
|
+
#
|
|
39
|
+
# # good
|
|
40
|
+
# User.new(name: "John")
|
|
41
|
+
#
|
|
42
|
+
class RuboCop::Cop::Callbacksystems::CollapseMultilineExpression < RuboCop::Cop::Callbacksystems::Base
|
|
43
|
+
extend RuboCop::Cop::AutoCorrector
|
|
44
|
+
|
|
45
|
+
def on_hash(node)
|
|
46
|
+
report_if_uncommented CollapsibleExpression.new(SingleLineLiteral.new(node, max_line_length, HASH_SHAPE)), node:
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def on_array(node)
|
|
50
|
+
report_if_uncommented CollapsibleExpression.new(SingleLineLiteral.new(node, max_line_length, ARRAY_SHAPE)), node:
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def on_send(node)
|
|
54
|
+
report_if_uncommented \
|
|
55
|
+
CollapsibleExpression.new(RuboCop::Callbacksystems::Source::SingleLineCall.new(node, max_line_length)),
|
|
56
|
+
node:
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
alias on_csend on_send
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
Shape = Data.define(:open, :close, :items)
|
|
63
|
+
|
|
64
|
+
HASH_SHAPE = Shape.new(open: "{", close: "}", items: :children)
|
|
65
|
+
ARRAY_SHAPE = Shape.new(open: "[", close: "]", items: :values)
|
|
66
|
+
|
|
67
|
+
def report_if_uncommented(analysis, node:)
|
|
68
|
+
report analysis if node.multiline? && !comments_within?(node)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def comments_within?(node)
|
|
72
|
+
source_comments.any_within?(node)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
class CollapsibleExpression
|
|
76
|
+
MESSAGE = "This expression can fit on a single line."
|
|
77
|
+
|
|
78
|
+
def initialize(single_line)
|
|
79
|
+
@single_line = single_line
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def offense
|
|
83
|
+
RuboCop::Callbacksystems::Offense.new(single_line.node, MESSAGE) { correct(it) } if single_line.fits?
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
attr_reader :single_line
|
|
88
|
+
|
|
89
|
+
def correct(corrector)
|
|
90
|
+
corrector.replace(single_line.node, single_line.source)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
class SingleLineLiteral
|
|
95
|
+
include RuboCop::Callbacksystems::Helpers
|
|
96
|
+
|
|
97
|
+
attr_reader :node
|
|
98
|
+
|
|
99
|
+
def initialize(node, max_line_length, shape)
|
|
100
|
+
@node = node
|
|
101
|
+
@max_line_length = max_line_length
|
|
102
|
+
@shape = shape
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def fits?
|
|
106
|
+
delimiter_match? && multiline? && no_multiline_items? && !carries_heredoc?(node) && fits_on_one_line?
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def source
|
|
110
|
+
@source ||= "#{shape.open} #{items_source} #{shape.close}"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
private
|
|
114
|
+
attr_reader :max_line_length, :shape
|
|
115
|
+
|
|
116
|
+
def delimiter_match?
|
|
117
|
+
node.loc.begin&.source == shape.open
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def multiline?
|
|
121
|
+
!node.single_line?
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def no_multiline_items?
|
|
125
|
+
items.all?(&:single_line?)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def items
|
|
129
|
+
node.public_send(shape.items)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def fits_on_one_line?
|
|
133
|
+
fits_on_line?(node, source, max_line_length)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def items_source
|
|
137
|
+
items.map { it.source.strip }.join(", ")
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|