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,137 @@
|
|
|
1
|
+
# A long comment block is prose doing work the code should do. Inside a class
|
|
2
|
+
# body, two lines are enough for the non-obvious why. A block sitting
|
|
3
|
+
# immediately above a top-level class or module sometimes holds two of those
|
|
4
|
+
# whys, so it gets four. A block inside a `test` block narrates the scenario,
|
|
5
|
+
# so it has no limit there. Shortening prose is human judgment, so there is no
|
|
6
|
+
# autocorrection.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# # bad - a three-line block inside a class body
|
|
10
|
+
# class Widget
|
|
11
|
+
# # The cache is warmed by the nightly job, and a miss here
|
|
12
|
+
# # means the job never ran, so we raise instead of recomputing
|
|
13
|
+
# # to surface the broken schedule.
|
|
14
|
+
# def price
|
|
15
|
+
# end
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
# # good - two lines say the non-obvious why
|
|
19
|
+
# class Widget
|
|
20
|
+
# # A cache miss means the nightly job never ran, so we raise
|
|
21
|
+
# # to surface the broken schedule instead of recomputing.
|
|
22
|
+
# def price
|
|
23
|
+
# end
|
|
24
|
+
# end
|
|
25
|
+
#
|
|
26
|
+
class RuboCop::Cop::Callbacksystems::CommentBlockLength < RuboCop::Cop::Callbacksystems::Base
|
|
27
|
+
def on_new_investigation
|
|
28
|
+
@comment_context = CommentContext.new(processed_source.ast)
|
|
29
|
+
comment_blocks_in(processed_source).each { report measured_block_of(it) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
attr_reader :comment_context
|
|
34
|
+
|
|
35
|
+
def measured_block_of(comments)
|
|
36
|
+
MeasuredBlock.new(comments, comment_context, max: cop_config["Max"], header_max: cop_config["HeaderMax"])
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
class MeasuredBlock
|
|
40
|
+
include RuboCop::Callbacksystems::Helpers
|
|
41
|
+
|
|
42
|
+
MESSAGE = "This comment block spans %d lines where %d is the limit. Keep the non-obvious why and drop any " \
|
|
43
|
+
"narration of the what."
|
|
44
|
+
|
|
45
|
+
def initialize(comments, context, max:, header_max:)
|
|
46
|
+
@comments = comments
|
|
47
|
+
@context = context
|
|
48
|
+
@max = max
|
|
49
|
+
@header_max = header_max
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def offense
|
|
53
|
+
RuboCop::Callbacksystems::Offense.new(range, message) if comments.size > limit && !inside_test?(range)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
attr_reader :comments, :context, :max, :header_max
|
|
58
|
+
delegate :inside_test?, to: :context, private: true
|
|
59
|
+
|
|
60
|
+
def limit
|
|
61
|
+
top_level_header? ? header_max : max
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def top_level_header?
|
|
65
|
+
context.top_level_header_on?(comments.last.source_range.line)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def range
|
|
69
|
+
range_spanning(comments)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def message
|
|
73
|
+
format(MESSAGE, comments.size, limit)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# File-wide comment contexts indexed once for all blocks in the investigation.
|
|
78
|
+
class CommentContext
|
|
79
|
+
extend RuboCop::AST::NodePattern::Macros
|
|
80
|
+
include RuboCop::Callbacksystems::Testing::CopHelpers
|
|
81
|
+
include RuboCop::Callbacksystems::Helpers
|
|
82
|
+
|
|
83
|
+
def initialize(ast)
|
|
84
|
+
@ast = ast
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def top_level_header_on?(line)
|
|
88
|
+
top_level_header_lines.include?(line)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def inside_test?(range)
|
|
92
|
+
test_ranges.include?(range)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
attr_reader :ast
|
|
97
|
+
|
|
98
|
+
def top_level_header_lines
|
|
99
|
+
@top_level_header_lines ||= nodes_in(ast, :class, :module).filter_map do |node|
|
|
100
|
+
node.first_line.pred if enclosing_class_or_module_of(node).nil?
|
|
101
|
+
end.to_set
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def test_ranges
|
|
105
|
+
@test_ranges ||= ContainingRanges.new(test_blocks(ast).map(&:source_range))
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Source ranges supporting containment queries in logarithmic time, including nested ranges.
|
|
109
|
+
class ContainingRanges
|
|
110
|
+
def initialize(ranges)
|
|
111
|
+
@ranges = ranges.sort_by(&:begin_pos)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def include?(range)
|
|
115
|
+
candidate = first_range_after(range.begin_pos).pred
|
|
116
|
+
candidate >= 0 && maximum_end_positions[candidate] >= range.end_pos
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
private
|
|
120
|
+
attr_reader :ranges
|
|
121
|
+
|
|
122
|
+
def first_range_after(position)
|
|
123
|
+
begin_positions.bsearch_index { it > position } || ranges.size
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def begin_positions
|
|
127
|
+
@begin_positions ||= ranges.map(&:begin_pos)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def maximum_end_positions
|
|
131
|
+
@maximum_end_positions ||= ranges.each_with_object([]) do |range, positions|
|
|
132
|
+
positions << [ positions.last || 0, range.end_pos ].max
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Detects complex conditionals with too many boolean operators. A condition
|
|
2
|
+
# chaining several operators has to be read term by term to know what it asks,
|
|
3
|
+
# while a predicate method names the question and holds the terms where they
|
|
4
|
+
# read on their own.
|
|
5
|
+
#
|
|
6
|
+
# @example MaxOperators: 1 (default)
|
|
7
|
+
# # bad - too many operators
|
|
8
|
+
# if a && b && c
|
|
9
|
+
# # ...
|
|
10
|
+
# end
|
|
11
|
+
#
|
|
12
|
+
# # good - extract to predicate method
|
|
13
|
+
# if complex_condition?
|
|
14
|
+
# # ...
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# def complex_condition?
|
|
18
|
+
# a && b && c
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# # good - single operator
|
|
22
|
+
# if a && b
|
|
23
|
+
# # ...
|
|
24
|
+
# end
|
|
25
|
+
#
|
|
26
|
+
class RuboCop::Cop::Callbacksystems::ComplexConditional < RuboCop::Cop::Callbacksystems::Base
|
|
27
|
+
MESSAGE = "Conditional has %<count>d boolean operators (max %<max>d). Extract to a predicate method."
|
|
28
|
+
|
|
29
|
+
def on_if(node)
|
|
30
|
+
check_condition(node.condition)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
alias on_while on_if
|
|
34
|
+
alias on_until on_if
|
|
35
|
+
alias on_while_post on_if
|
|
36
|
+
alias on_until_post on_if
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
def check_condition(condition)
|
|
40
|
+
count = OperatorCount.new(condition).value
|
|
41
|
+
add_offense(condition, message: format(MESSAGE, count: count, max: max_operators)) if count > max_operators
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def max_operators
|
|
45
|
+
cop_config["MaxOperators"]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# The top-level boolean spine, walked iteratively so arbitrarily deep parser output cannot exhaust the call stack.
|
|
49
|
+
class OperatorCount
|
|
50
|
+
def initialize(node)
|
|
51
|
+
@pending = [ node ]
|
|
52
|
+
@count = 0
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def value
|
|
56
|
+
visit(pending.pop) until pending.empty?
|
|
57
|
+
count
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
attr_reader :pending, :count
|
|
62
|
+
|
|
63
|
+
def visit(node)
|
|
64
|
+
case node&.type
|
|
65
|
+
when :and, :or then count_operator(node)
|
|
66
|
+
when :begin, :kwbegin then pending << node.children.last
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def count_operator(node)
|
|
71
|
+
@count += 1
|
|
72
|
+
pending.concat(node.children)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# In controller tests, every test that makes an HTTP request must assert the response.
|
|
2
|
+
# Testing only side effects without verifying the response status or redirect
|
|
3
|
+
# leaves the controller's behavior unverified.
|
|
4
|
+
#
|
|
5
|
+
# @example
|
|
6
|
+
# # bad - no response assertion
|
|
7
|
+
# test "create creates a user" do
|
|
8
|
+
# post users_url, params: { name: "John" }
|
|
9
|
+
# assert_equal 1, User.count
|
|
10
|
+
# end
|
|
11
|
+
#
|
|
12
|
+
# # good - asserts response status
|
|
13
|
+
# test "create creates a user" do
|
|
14
|
+
# post users_url, params: { name: "John" }
|
|
15
|
+
# assert_response :created
|
|
16
|
+
# assert_equal 1, User.count
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# # good - asserts redirect
|
|
20
|
+
# test "create redirects" do
|
|
21
|
+
# post users_url, params: { name: "John" }
|
|
22
|
+
# assert_redirected_to users_path
|
|
23
|
+
# end
|
|
24
|
+
#
|
|
25
|
+
# # good - no HTTP request (not a controller action test)
|
|
26
|
+
# test "helper works" do
|
|
27
|
+
# assert_equal "expected", helper_method
|
|
28
|
+
# end
|
|
29
|
+
#
|
|
30
|
+
class RuboCop::Cop::Callbacksystems::ControllerTestResponseAssertion < RuboCop::Cop::Callbacksystems::Base
|
|
31
|
+
include RuboCop::Callbacksystems::Testing::CopHelpers
|
|
32
|
+
|
|
33
|
+
def on_block(node)
|
|
34
|
+
report_each RequestResponses.new(node)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
alias on_numblock on_block
|
|
38
|
+
alias on_itblock on_block
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
REQUEST_ABORTING_ASSERTIONS = %i[ assert_raises ]
|
|
42
|
+
|
|
43
|
+
class RequestResponses
|
|
44
|
+
extend RuboCop::AST::NodePattern::Macros
|
|
45
|
+
include RuboCop::Callbacksystems::Testing::CopHelpers
|
|
46
|
+
include RuboCop::Callbacksystems::Helpers
|
|
47
|
+
|
|
48
|
+
MESSAGE = "Controller test makes an HTTP request but has no response assertion (assert_response or " \
|
|
49
|
+
"assert_redirected_to)."
|
|
50
|
+
|
|
51
|
+
def initialize(node)
|
|
52
|
+
@node = node
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def each_offense
|
|
56
|
+
missing_requests.each { yield RuboCop::Callbacksystems::Offense.new(it, MESSAGE) }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
attr_reader :node
|
|
61
|
+
|
|
62
|
+
def missing_requests
|
|
63
|
+
if test_block?(node)
|
|
64
|
+
request_runs.filter_map do |run|
|
|
65
|
+
request = run.first
|
|
66
|
+
request if request_statement?(request) && !request_answered_in?(run)
|
|
67
|
+
end
|
|
68
|
+
else
|
|
69
|
+
[]
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def request_runs
|
|
74
|
+
statements.slice_before { request_statement?(it) }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def statements
|
|
78
|
+
@statements ||= statements_in(node.body)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def request_statement?(statement)
|
|
82
|
+
immediate_calls_in(statement).any? { http_request?(it) }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def request_answered_in?(run)
|
|
86
|
+
run.any? { response_assertion_in?(it) } || request_aborted_in?(run.first)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def response_assertion_in?(statement)
|
|
90
|
+
immediate_calls_in(statement).any? { response_assertion?(it) }
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def request_aborted_in?(statement)
|
|
94
|
+
immediate_calls_in(statement).select { http_request?(it) }.all? do |request|
|
|
95
|
+
request.each_ancestor(:any_block)
|
|
96
|
+
.take_while { it.equal?(statement) || statement.source_range.contains?(it.source_range) }
|
|
97
|
+
.any? { request_aborting_assertion?(it.send_node) }
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def request_aborting_assertion?(send_node)
|
|
102
|
+
call_on_self?(send_node) && REQUEST_ABORTING_ASSERTIONS.include?(send_node.method_name)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# In controller tests, the first assertion after an HTTP request must be a
|
|
2
|
+
# response assertion (assert_response or assert_redirected_to). The response is
|
|
3
|
+
# what the request produced, so it is read first: when the controller fails, a
|
|
4
|
+
# side-effect assertion placed before it fails on a symptom (a missing record)
|
|
5
|
+
# and hides the status that explains it. Non-assertion code between the request
|
|
6
|
+
# and the response assertion is allowed, and so are ActiveJob and ActionMailer
|
|
7
|
+
# assertions (assert_enqueued_jobs, assert_enqueued_emails, etc.).
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# # bad - first assertion is not about the response
|
|
11
|
+
# test "create" do
|
|
12
|
+
# post users_url, params: { name: "John" }
|
|
13
|
+
# assert_equal 1, User.count
|
|
14
|
+
# assert_response :created
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# # good - response assertion comes first
|
|
18
|
+
# test "create" do
|
|
19
|
+
# post users_url, params: { name: "John" }
|
|
20
|
+
# assert_response :created
|
|
21
|
+
# assert_equal 1, User.count
|
|
22
|
+
# end
|
|
23
|
+
#
|
|
24
|
+
# # good - non-assertion code before response assertion is fine
|
|
25
|
+
# test "create" do
|
|
26
|
+
# post users_url, params: { name: "John" }
|
|
27
|
+
# user = User.last
|
|
28
|
+
# assert_response :created
|
|
29
|
+
# assert_equal "John", user.name
|
|
30
|
+
# end
|
|
31
|
+
#
|
|
32
|
+
# # good - assert_difference wrapping request, then response assertion
|
|
33
|
+
# test "create" do
|
|
34
|
+
# assert_difference("User.count", 1) do
|
|
35
|
+
# post users_url, params: { name: "John" }
|
|
36
|
+
# end
|
|
37
|
+
# assert_response :created
|
|
38
|
+
# end
|
|
39
|
+
#
|
|
40
|
+
# # good - ActiveJob/ActionMailer assertions before response assertion
|
|
41
|
+
# test "create sends email" do
|
|
42
|
+
# post users_url, params: { name: "John" }
|
|
43
|
+
# assert_enqueued_emails 1
|
|
44
|
+
# assert_response :created
|
|
45
|
+
# end
|
|
46
|
+
#
|
|
47
|
+
class RuboCop::Cop::Callbacksystems::ControllerTestResponseFirstAssertion < RuboCop::Cop::Callbacksystems::Base
|
|
48
|
+
include RuboCop::Callbacksystems::Testing::CopHelpers
|
|
49
|
+
|
|
50
|
+
def on_block(node)
|
|
51
|
+
report_each RequestAssertions.new(node) if test_block?(node)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
alias on_numblock on_block
|
|
55
|
+
alias on_itblock on_block
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
SIDE_EFFECT_ASSERTIONS = %i[
|
|
59
|
+
assert_enqueued_jobs assert_no_enqueued_jobs assert_performed_jobs assert_no_performed_jobs
|
|
60
|
+
assert_enqueued_with assert_performed_with
|
|
61
|
+
assert_emails assert_no_emails assert_enqueued_emails assert_no_enqueued_emails assert_enqueued_email_with
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
# The assertions a test reads first after each of its requests, kept when one is not about the response.
|
|
65
|
+
class RequestAssertions
|
|
66
|
+
extend RuboCop::AST::NodePattern::Macros
|
|
67
|
+
include RuboCop::Callbacksystems::Testing::CopHelpers
|
|
68
|
+
include RuboCop::Callbacksystems::Helpers
|
|
69
|
+
|
|
70
|
+
MESSAGE = "The first assertion after an HTTP request must be `assert_response` or `assert_redirected_to`, not " \
|
|
71
|
+
"`%<method>s`."
|
|
72
|
+
|
|
73
|
+
def initialize(node)
|
|
74
|
+
@node = node
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def each_offense
|
|
78
|
+
misplaced_assertions.each { yield RuboCop::Callbacksystems::Offense.new(it, message_for(it)) }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
attr_reader :node
|
|
83
|
+
|
|
84
|
+
def misplaced_assertions
|
|
85
|
+
waiting_for_assertion = false
|
|
86
|
+
statements.filter_map do |statement|
|
|
87
|
+
if http_request_statement?(statement)
|
|
88
|
+
waiting_for_assertion = true
|
|
89
|
+
nil
|
|
90
|
+
elsif waiting_for_assertion
|
|
91
|
+
first_assertion_in(statement)&.then do |assertion|
|
|
92
|
+
waiting_for_assertion = false
|
|
93
|
+
assertion unless response_assertion?(assertion)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def statements
|
|
100
|
+
@statements ||= statements_in(node.body)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def http_request_statement?(statement)
|
|
104
|
+
immediate_calls_in(statement).any? { http_request?(it) }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def first_assertion_in(statement)
|
|
108
|
+
immediate_calls_in(statement).find { assertion?(it) && !side_effect_assertion?(it) }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def assertion?(send_node)
|
|
112
|
+
call_on_self?(send_node) && send_node.method_name.to_s.start_with?("assert", "refute")
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def side_effect_assertion?(send_node)
|
|
116
|
+
call_on_self?(send_node) && SIDE_EFFECT_ASSERTIONS.include?(send_node.method_name)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def message_for(assertion)
|
|
120
|
+
format(MESSAGE, method: assertion.method_name)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|