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,336 @@
|
|
|
1
|
+
# Ensures a `module:` option repeated across routes is extracted to a `scope`
|
|
2
|
+
# block. Repeated, the namespace sits on every line and a reader compares them
|
|
3
|
+
# to learn that the routes share a controller directory, where one
|
|
4
|
+
# `scope module:` says it once and lets the routes read bare.
|
|
5
|
+
#
|
|
6
|
+
# Autocorrection consolidates the routes only when they are contiguous, so it
|
|
7
|
+
# never reorders a route past another and changes matching precedence.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# # bad - module: repeated multiple times
|
|
11
|
+
# resources :users, module: :admin
|
|
12
|
+
# resources :posts, module: :admin
|
|
13
|
+
# resources :comments, module: :admin
|
|
14
|
+
#
|
|
15
|
+
# # good - use scope block
|
|
16
|
+
# scope module: :admin do
|
|
17
|
+
# resources :users
|
|
18
|
+
# resources :posts
|
|
19
|
+
# resources :comments
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
class RuboCop::Cop::Callbacksystems::RoutesModuleScope < RuboCop::Cop::Callbacksystems::Base
|
|
23
|
+
extend RuboCop::Cop::AutoCorrector
|
|
24
|
+
|
|
25
|
+
def on_new_investigation
|
|
26
|
+
if routes_file?(processed_source.file_path)
|
|
27
|
+
@source_comments = RuboCop::Callbacksystems::Source::Comments.for(processed_source)
|
|
28
|
+
@statement_positions = StatementPositions.new
|
|
29
|
+
repeated_route_groups.each { report_each RouteGroup.new(it, source_comments:, statement_positions:) }
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
ROUTE_METHODS = %i[ resources resource get post put patch delete match root ]
|
|
35
|
+
|
|
36
|
+
attr_reader :source_comments, :statement_positions
|
|
37
|
+
|
|
38
|
+
def repeated_route_groups
|
|
39
|
+
routes_with_module.group_by(&:group_key).values.select(&:many?)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def routes_with_module
|
|
43
|
+
route_calls.map { Route.new(it) }.select(&:module_value)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def route_calls
|
|
47
|
+
RuboCop::Callbacksystems::Execution::Immediate.new(processed_source.ast).nodes_of_type(:send)
|
|
48
|
+
.select { bare_send?(it) && ROUTE_METHODS.include?(it.method_name) }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Routes repeating one `module:`, where only the first offense carries the correction that rewrites the whole run.
|
|
52
|
+
class RouteGroup
|
|
53
|
+
MESSAGE = "Extract repeated `module: %<module>s` to a `scope module: %<module>s do` block."
|
|
54
|
+
|
|
55
|
+
def initialize(routes, source_comments:, statement_positions:)
|
|
56
|
+
@routes = routes
|
|
57
|
+
@source_comments = source_comments
|
|
58
|
+
@statement_positions = statement_positions
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def each_offense
|
|
62
|
+
yield offense_for(routes.first, correcting: consolidation.applicable?)
|
|
63
|
+
routes.drop(1).each { yield offense_for(it, correcting: false) }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
attr_reader :routes, :source_comments, :statement_positions
|
|
68
|
+
|
|
69
|
+
def offense_for(route, correcting:)
|
|
70
|
+
RuboCop::Callbacksystems::Offense.new(route.node, message_for(route), correcting:) { consolidation.rewrite(it) }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def message_for(route)
|
|
74
|
+
format(MESSAGE, module: route.module_value)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def consolidation
|
|
78
|
+
@consolidation ||= Consolidation.new(routes, source_comments:, statement_positions:)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# One group of routes rewritten as a `scope module:` block, when they sit together with no comment among them.
|
|
83
|
+
class Consolidation
|
|
84
|
+
include RuboCop::Callbacksystems::Helpers
|
|
85
|
+
|
|
86
|
+
def initialize(routes, source_comments:, statement_positions:)
|
|
87
|
+
@routes = routes
|
|
88
|
+
@source_comments = source_comments
|
|
89
|
+
@statement_positions = statement_positions
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def applicable?
|
|
93
|
+
contiguous? && routes.all?(&:literal_module?) && routes.all?(&:module_removable?) && source_self_contained? &&
|
|
94
|
+
!comments_within_span?
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def rewrite(corrector)
|
|
98
|
+
corrector.replace(span, scope_block)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
attr_reader :routes, :source_comments, :statement_positions
|
|
103
|
+
|
|
104
|
+
def contiguous?
|
|
105
|
+
container.begin_type? && nodes.all? { it.parent.equal?(container) } && consecutive?(positions_in_container)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def container
|
|
109
|
+
@container ||= nodes.first.parent
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def nodes
|
|
113
|
+
@nodes ||= routes.map(&:node)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def consecutive?(indexes)
|
|
117
|
+
indexes.sort.each_cons(2).all? { |left, right| right == left + 1 }
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def positions_in_container
|
|
121
|
+
nodes.map { statement_positions.position_of(it) }
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def source_self_contained?
|
|
125
|
+
routes.none? { carries_heredoc?(it.node) }
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def comments_within_span?
|
|
129
|
+
source_comments.any_on_lines?(span.first_line..span.last_line)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def span
|
|
133
|
+
@span ||= range_spanning(ordered_routes.map(&:node))
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def ordered_routes
|
|
137
|
+
@ordered_routes ||= routes.sort_by { it.node.source_range.begin_pos }
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def scope_block
|
|
141
|
+
[ "scope module: #{module_source} do", *route_lines, "#{indent}end" ].join("\n")
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def module_source
|
|
145
|
+
routes.first.module_value
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def route_lines
|
|
149
|
+
ordered_routes.map { "#{indent} #{it.source_without_module}" }
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def indent
|
|
153
|
+
@indent ||= indentation_of(ordered_routes.first.node)
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Statement positions shared by route groups, preserving node identity when two routes have identical syntax.
|
|
158
|
+
class StatementPositions
|
|
159
|
+
def initialize
|
|
160
|
+
@by_container = {}.compare_by_identity
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def position_of(node)
|
|
164
|
+
positions_in(node.parent).fetch(node)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
private
|
|
168
|
+
attr_reader :by_container
|
|
169
|
+
|
|
170
|
+
def positions_in(container)
|
|
171
|
+
by_container[container] ||= positions_of(container.children)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def positions_of(statements)
|
|
175
|
+
statements.each_with_index.with_object({}.compare_by_identity) do |(statement, position), positions|
|
|
176
|
+
positions[statement] = position
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
class Route
|
|
182
|
+
include RuboCop::Callbacksystems::Helpers
|
|
183
|
+
|
|
184
|
+
attr_reader :node
|
|
185
|
+
|
|
186
|
+
def initialize(node)
|
|
187
|
+
@node = node
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def group_key
|
|
191
|
+
[ parent_block, normalized_module ]
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def module_value
|
|
195
|
+
module_node&.source
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def literal_module?
|
|
199
|
+
module_node.type?(:sym, :str)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def module_removable?
|
|
203
|
+
module_pairs.one?
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def source_without_module
|
|
207
|
+
before_module.source + after_module.source
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
private
|
|
211
|
+
# Two blocks written alike compare equal as nodes, so the position tells the parents apart.
|
|
212
|
+
def parent_block
|
|
213
|
+
node.each_ancestor(*BLOCK_NODE_TYPES).first&.then { it.source_range.begin_pos } || :root
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def normalized_module
|
|
217
|
+
[ module_node.type, literal_name_of(module_node) ]
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def module_node
|
|
221
|
+
module_pair&.value
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def module_pair
|
|
225
|
+
keyword_options.option_for(:module)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def keyword_options
|
|
229
|
+
@keyword_options ||= RuboCop::Callbacksystems::Hashes::KeywordOptions.new(*node.arguments.select(&:hash_type?))
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def module_pairs
|
|
233
|
+
keyword_options.explicit_options_for(:module)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def before_module
|
|
237
|
+
node.source_range.with(end_pos: module_gap.begin_pos)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def module_gap
|
|
241
|
+
explicit_options? ? explicit_module_gap : implicit_module_gap
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def explicit_options?
|
|
245
|
+
options.loc.begin
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def options
|
|
249
|
+
module_pair.parent
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def explicit_module_gap
|
|
253
|
+
if option_after_module
|
|
254
|
+
module_pair.source_range.with(end_pos: option_after_module.source_range.begin_pos)
|
|
255
|
+
elsif option_before_module
|
|
256
|
+
option_before_module.source_range.end.join(module_pair.source_range.end)
|
|
257
|
+
else
|
|
258
|
+
options_argument_gap
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def option_after_module
|
|
263
|
+
option_elements[option_index + 1]
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def option_elements
|
|
267
|
+
options.children
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def option_index
|
|
271
|
+
option_elements.index { it.equal?(module_pair) }
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def option_before_module
|
|
275
|
+
option_elements[option_index - 1] if option_index.positive?
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def options_argument_gap
|
|
279
|
+
following_options_gap || preceding_options_gap || sole_options_gap
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def following_options_gap
|
|
283
|
+
argument_after_options&.then { options.source_range.with(end_pos: it.source_range.begin_pos) }
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def argument_after_options
|
|
287
|
+
node.arguments[options_index + 1]
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def options_index
|
|
291
|
+
node.arguments.index { it.equal?(options) }
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def preceding_options_gap
|
|
295
|
+
argument_before_options&.then { it.source_range.end.join(options.source_range.end) }
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def argument_before_options
|
|
299
|
+
node.arguments[options_index - 1] if options_index.positive?
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def sole_options_gap
|
|
303
|
+
node.loc.begin.join(node.loc.end)
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def implicit_module_gap
|
|
307
|
+
if element_after_module
|
|
308
|
+
module_pair.source_range.join(element_after_module.source_range.begin)
|
|
309
|
+
elsif element_before_module
|
|
310
|
+
element_before_module.source_range.end.join(module_pair.source_range)
|
|
311
|
+
else
|
|
312
|
+
node.loc.selector.end.join(node.source_range.end)
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def element_after_module
|
|
317
|
+
elements[module_index + 1]
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def elements
|
|
321
|
+
@elements ||= node.arguments.flat_map { it.hash_type? ? it.children : [ it ] }
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def module_index
|
|
325
|
+
elements.index { it.equal?(module_pair) }
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def element_before_module
|
|
329
|
+
elements[module_index - 1] if module_index.positive?
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def after_module
|
|
333
|
+
node.source_range.with(begin_pos: module_gap.end_pos)
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# Asks for `{ }` over `do end` on a single-line setup or teardown block. The
|
|
2
|
+
# `do end` form is the shape of a body with several lines, so wrapping one
|
|
3
|
+
# statement in it spends two lines on a body that never grew.
|
|
4
|
+
#
|
|
5
|
+
# @example
|
|
6
|
+
# # bad
|
|
7
|
+
# setup do
|
|
8
|
+
# @user = users(:bruno)
|
|
9
|
+
# end
|
|
10
|
+
#
|
|
11
|
+
# # good
|
|
12
|
+
# setup { @user = users(:bruno) }
|
|
13
|
+
#
|
|
14
|
+
# # good (multi-line is fine with do/end)
|
|
15
|
+
# setup do
|
|
16
|
+
# @user = users(:bruno)
|
|
17
|
+
# @account = accounts(:callback)
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
class RuboCop::Cop::Callbacksystems::SingleLineSetupBlock < RuboCop::Cop::Callbacksystems::Base
|
|
21
|
+
extend RuboCop::Cop::AutoCorrector
|
|
22
|
+
|
|
23
|
+
def on_new_investigation
|
|
24
|
+
@source_comments = RuboCop::Callbacksystems::Source::Comments.for(processed_source)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def on_block(node)
|
|
28
|
+
report SetupBlock.new(node, @source_comments)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
alias on_numblock on_block
|
|
32
|
+
alias on_itblock on_block
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
class SetupBlock
|
|
36
|
+
include RuboCop::Callbacksystems::Helpers
|
|
37
|
+
extend RuboCop::AST::NodePattern::Macros
|
|
38
|
+
|
|
39
|
+
MESSAGE = "Use `%<method>s { ... }` for single-line blocks instead of `do ... end`."
|
|
40
|
+
|
|
41
|
+
# @!method setup_or_teardown_block?(node)
|
|
42
|
+
def_node_matcher :setup_or_teardown_block?, <<~PATTERN
|
|
43
|
+
(any_block (send nil? ${:setup :teardown} ...) ...)
|
|
44
|
+
PATTERN
|
|
45
|
+
|
|
46
|
+
def initialize(node, source_comments)
|
|
47
|
+
@node = node
|
|
48
|
+
@source_comments = source_comments
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def offense
|
|
52
|
+
offense_for_block if expanded_one_liner?
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
attr_reader :node, :source_comments
|
|
57
|
+
|
|
58
|
+
def expanded_one_liner?
|
|
59
|
+
setup_or_teardown_block?(node) && !node.braces? && node.body&.single_line? && statements_in(node.body).one?
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def offense_for_block
|
|
63
|
+
RuboCop::Callbacksystems::Offense.new(node, message, correcting: correctable?) { correct(it) }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def message
|
|
67
|
+
format(MESSAGE, method: node.method_name)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Braces bind tighter than command-style arguments, so a multiline command stays untouched until its comments
|
|
71
|
+
# and indentation can be preserved. A single-line command can become an explicit argument list.
|
|
72
|
+
def correctable?
|
|
73
|
+
comments_movable? && !carries_heredoc?(node.body) &&
|
|
74
|
+
(node.send_node.arguments.empty? || node.send_node.parenthesized? || node.send_node.single_line?)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# One prose comment has an unambiguous home after the compact block. Several comments encode a layout of their
|
|
78
|
+
# own, and tooling directives can change meaning when moved, so neither is rewritten.
|
|
79
|
+
def comments_movable?
|
|
80
|
+
comments_on_block_lines.size <= 1 && comments_on_block_lines.none? { tooling_comment?(it) }
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def comments_on_block_lines
|
|
84
|
+
@comments_on_block_lines ||= source_comments.on_lines(node.first_line..node.last_line)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def correct(corrector)
|
|
88
|
+
corrector.replace(node, "#{call_source} { #{parameters}#{node.body.source.strip} }")
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def call_source
|
|
92
|
+
if node.send_node.arguments? && !node.send_node.parenthesized?
|
|
93
|
+
"#{node.method_name}(#{call_arguments_source})"
|
|
94
|
+
else
|
|
95
|
+
node.send_node.source
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def call_arguments_source
|
|
100
|
+
node.send_node.first_argument.source_range.join(node.send_node.last_argument.source_range).source
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def parameters
|
|
104
|
+
"#{node.arguments.source} " unless node.arguments.empty?
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# A class declares its class-level methods in a single `class << self` section.
|
|
2
|
+
# Splitting them across several blocks scatters the class API and hides which
|
|
3
|
+
# part of it is private.
|
|
4
|
+
#
|
|
5
|
+
# @example
|
|
6
|
+
# # bad - two singleton sections
|
|
7
|
+
# class Configuration
|
|
8
|
+
# class << self
|
|
9
|
+
# def create_from(file)
|
|
10
|
+
# end
|
|
11
|
+
# end
|
|
12
|
+
#
|
|
13
|
+
# def initialize(raw_config)
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# class << self
|
|
17
|
+
# private
|
|
18
|
+
# def load_config_files(file)
|
|
19
|
+
# end
|
|
20
|
+
# end
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# # good - one singleton section
|
|
24
|
+
# class Configuration
|
|
25
|
+
# class << self
|
|
26
|
+
# def create_from(file)
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
# private
|
|
30
|
+
# def load_config_files(file)
|
|
31
|
+
# end
|
|
32
|
+
# end
|
|
33
|
+
#
|
|
34
|
+
# def initialize(raw_config)
|
|
35
|
+
# end
|
|
36
|
+
# end
|
|
37
|
+
#
|
|
38
|
+
class RuboCop::Cop::Callbacksystems::SingleSingletonClass < RuboCop::Cop::Callbacksystems::Base
|
|
39
|
+
MESSAGE = "Merge this `class << self` into the one declared above; a class has a single singleton section."
|
|
40
|
+
|
|
41
|
+
def on_class(node)
|
|
42
|
+
singleton_sections_in(node).drop(1).each { add_offense(it, message: MESSAGE) }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
def singleton_sections_in(node)
|
|
47
|
+
statements_in(node.body).select { singleton_section?(it) }
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# Forbids a second class or module at the top level of a file. Zeitwerk expects
|
|
2
|
+
# the path of a file to name the constant it defines, so a second definition is
|
|
3
|
+
# one nothing autoloads, and a reader looking for it by name never finds the
|
|
4
|
+
# file it sits in.
|
|
5
|
+
#
|
|
6
|
+
# A class extending one written above it in the same file is left alone. Moving it
|
|
7
|
+
# out would need a `require` back, so the two sitting together is a hierarchy
|
|
8
|
+
# rather than the sprawl this looks for.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# # bad - multiple top-level classes
|
|
12
|
+
# class User
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# class Admin
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
# # bad - multiple top-level modules
|
|
19
|
+
# module Authentication
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
# module Authorization
|
|
23
|
+
# end
|
|
24
|
+
#
|
|
25
|
+
# # bad - mixed top-level class and module
|
|
26
|
+
# class User
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
# module UserHelpers
|
|
30
|
+
# end
|
|
31
|
+
#
|
|
32
|
+
# # good - single top-level class
|
|
33
|
+
# class User
|
|
34
|
+
# end
|
|
35
|
+
#
|
|
36
|
+
# # good - single top-level module
|
|
37
|
+
# module Authentication
|
|
38
|
+
# end
|
|
39
|
+
#
|
|
40
|
+
# # good - nested classes/modules inside one top-level
|
|
41
|
+
# class User
|
|
42
|
+
# class Profile
|
|
43
|
+
# end
|
|
44
|
+
#
|
|
45
|
+
# module Validations
|
|
46
|
+
# end
|
|
47
|
+
# end
|
|
48
|
+
#
|
|
49
|
+
# # good - a hierarchy the file declares together
|
|
50
|
+
# class BaseTest < ActiveSupport::TestCase
|
|
51
|
+
# end
|
|
52
|
+
#
|
|
53
|
+
# class VariantTest < BaseTest
|
|
54
|
+
# end
|
|
55
|
+
#
|
|
56
|
+
class RuboCop::Cop::Callbacksystems::SingleTopLevelClassOrModule < RuboCop::Cop::Callbacksystems::Base
|
|
57
|
+
def on_new_investigation
|
|
58
|
+
report_each TopLevelDefinitions.new(processed_source.ast)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
class TopLevelDefinitions
|
|
63
|
+
include RuboCop::Callbacksystems::Helpers
|
|
64
|
+
|
|
65
|
+
MESSAGE = "Only one top-level class or module is allowed per file."
|
|
66
|
+
TRANSPARENT_NODES = %i[
|
|
67
|
+
begin kwbegin if case case_match when in_pattern rescue resbody ensure
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
def initialize(ast)
|
|
71
|
+
@ast = ast
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def each_offense
|
|
75
|
+
standalone.each { yield RuboCop::Callbacksystems::Offense.new(it, MESSAGE) }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
attr_reader :ast
|
|
80
|
+
|
|
81
|
+
def standalone
|
|
82
|
+
definitions.drop(1).reject { it.extends?(definitions) }.map(&:node)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def definitions
|
|
86
|
+
@definitions ||= definitions_in(ast)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def definitions_in(node)
|
|
90
|
+
Enumerator.new do |definitions|
|
|
91
|
+
pending = [ node ].compact
|
|
92
|
+
until pending.empty?
|
|
93
|
+
current = pending.pop
|
|
94
|
+
definition = Definition.new(current)
|
|
95
|
+
if definition.declared?
|
|
96
|
+
definitions << definition
|
|
97
|
+
elsif current.type?(*TRANSPARENT_NODES)
|
|
98
|
+
pending.concat(current.child_nodes.reverse)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end.to_a
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
class Definition
|
|
105
|
+
include RuboCop::Callbacksystems::Helpers
|
|
106
|
+
|
|
107
|
+
attr_reader :node
|
|
108
|
+
|
|
109
|
+
def initialize(node)
|
|
110
|
+
@node = node
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def declared?
|
|
114
|
+
node.type?(:class, :module) || builder_declaration?
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def extends?(definitions)
|
|
118
|
+
parent_class.then { |parent| parent && definitions.any? { it.name == parent.source } }
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def name
|
|
122
|
+
node.type?(:class, :module) ? node.identifier.source : assigned_constant_name
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
private
|
|
126
|
+
def builder_declaration?
|
|
127
|
+
if node.casgn_type?
|
|
128
|
+
builder_assignment.builds_class? || builder_assignment.builds_module?
|
|
129
|
+
else
|
|
130
|
+
false
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def builder_assignment
|
|
135
|
+
@builder_assignment ||= RuboCop::Callbacksystems::ClassStructure::BuilderAssignment.new(node)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def parent_class
|
|
139
|
+
if node.class_type?
|
|
140
|
+
node.parent_class
|
|
141
|
+
elsif node.casgn_type? && class_new_builder?
|
|
142
|
+
builder_call.first_argument
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def class_new_builder?
|
|
147
|
+
builder_call&.method?(:new) && core_constant?(builder_call.receiver) &&
|
|
148
|
+
builder_call.receiver.short_name == :Class
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def builder_call
|
|
152
|
+
@builder_call ||= call_of(node.expression)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def assigned_constant_name
|
|
156
|
+
node.source_range.with(end_pos: node.loc.operator.begin_pos).source.rstrip
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|