rubocop-vibe 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/config/default.yml +126 -0
- data/lib/rubocop/cop/vibe/blank_line_before_expectation.rb +141 -0
- data/lib/rubocop/cop/vibe/class_organization.rb +609 -0
- data/lib/rubocop/cop/vibe/consecutive_assignment_alignment.rb +219 -0
- data/lib/rubocop/cop/vibe/describe_block_order.rb +262 -0
- data/lib/rubocop/cop/vibe/is_expected_one_liner.rb +108 -0
- data/lib/rubocop/cop/vibe/mixin/spec_file_helper.rb +20 -0
- data/lib/rubocop/cop/vibe/no_assigns_attribute_testing.rb +62 -0
- data/lib/rubocop/cop/vibe/no_rubocop_disable.rb +60 -0
- data/lib/rubocop/cop/vibe/no_skipped_tests.rb +115 -0
- data/lib/rubocop/cop/vibe/no_unless_guard_clause.rb +250 -0
- data/lib/rubocop/cop/vibe/prefer_one_liner_expectation.rb +185 -0
- data/lib/rubocop/cop/vibe/service_call_method.rb +82 -0
- data/lib/rubocop/cop/vibe_cops.rb +15 -0
- data/lib/rubocop/vibe/plugin.rb +41 -0
- data/lib/rubocop/vibe/version.rb +7 -0
- data/lib/rubocop/vibe.rb +9 -0
- data/lib/rubocop-vibe.rb +9 -0
- metadata +131 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Vibe
|
|
6
|
+
# Enforces alignment of consecutive variable assignments at the `=` operator.
|
|
7
|
+
#
|
|
8
|
+
# Consecutive assignments (with no blank lines between) should align their
|
|
9
|
+
# `=` operators for better readability. Groups are broken by blank lines.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# # bad
|
|
13
|
+
# user = create(:user)
|
|
14
|
+
# character = create(:character)
|
|
15
|
+
# input = "test"
|
|
16
|
+
#
|
|
17
|
+
# # good
|
|
18
|
+
# user = create(:user)
|
|
19
|
+
# character = create(:character)
|
|
20
|
+
# input = "test"
|
|
21
|
+
#
|
|
22
|
+
# # good - blank line breaks the group
|
|
23
|
+
# user = create(:user)
|
|
24
|
+
# character = create(:character)
|
|
25
|
+
#
|
|
26
|
+
# service = Users::Activate.new # Separate group, not aligned
|
|
27
|
+
class ConsecutiveAssignmentAlignment < Base
|
|
28
|
+
extend AutoCorrector
|
|
29
|
+
|
|
30
|
+
MSG = "Align consecutive assignments at the = operator."
|
|
31
|
+
|
|
32
|
+
# Check block nodes for assignment alignment.
|
|
33
|
+
#
|
|
34
|
+
# @param [RuboCop::AST::Node] node The block node.
|
|
35
|
+
# @return [void]
|
|
36
|
+
def on_block(node)
|
|
37
|
+
if node.body
|
|
38
|
+
check_assignments_in_body(node.body)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
alias on_numblock on_block
|
|
42
|
+
|
|
43
|
+
# Check method definitions for assignment alignment.
|
|
44
|
+
#
|
|
45
|
+
# @param [RuboCop::AST::Node] node The def node.
|
|
46
|
+
# @return [void]
|
|
47
|
+
def on_def(node)
|
|
48
|
+
if node.body
|
|
49
|
+
check_assignments_in_body(node.body)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
alias on_defs on_def
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
# Check assignments in a body node.
|
|
57
|
+
#
|
|
58
|
+
# @param [RuboCop::AST::Node] body The body node.
|
|
59
|
+
# @return [void]
|
|
60
|
+
def check_assignments_in_body(body)
|
|
61
|
+
statements = extract_statements(body)
|
|
62
|
+
return if statements.size < 2
|
|
63
|
+
|
|
64
|
+
groups = group_consecutive_assignments(statements)
|
|
65
|
+
groups.each { |group| check_group_alignment(group) }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Extract statements from a body node.
|
|
69
|
+
#
|
|
70
|
+
# @param [RuboCop::AST::Node] body The body node.
|
|
71
|
+
# @return [Array<RuboCop::AST::Node>]
|
|
72
|
+
def extract_statements(body)
|
|
73
|
+
if body.begin_type?
|
|
74
|
+
body.children
|
|
75
|
+
else
|
|
76
|
+
[body]
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Group consecutive assignments together.
|
|
81
|
+
#
|
|
82
|
+
# @param [Array<RuboCop::AST::Node>] statements The statements.
|
|
83
|
+
# @return [Array<Array<RuboCop::AST::Node>>] Groups of consecutive assignments.
|
|
84
|
+
def group_consecutive_assignments(statements)
|
|
85
|
+
groups = []
|
|
86
|
+
current_group = []
|
|
87
|
+
previous_line = nil
|
|
88
|
+
|
|
89
|
+
statements.each do |statement|
|
|
90
|
+
current_group, previous_line = process_statement(statement, current_group, previous_line, groups)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
finalize_groups(groups, current_group)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Process a single statement for grouping.
|
|
97
|
+
#
|
|
98
|
+
# @param [RuboCop::AST::Node] statement The statement.
|
|
99
|
+
# @param [Array<RuboCop::AST::Node>] current_group The current group.
|
|
100
|
+
# @param [Integer, nil] previous_line The previous line number.
|
|
101
|
+
# @param [Array<Array<RuboCop::AST::Node>>] groups The groups.
|
|
102
|
+
# @return [Array] The updated current_group and previous_line.
|
|
103
|
+
def process_statement(statement, current_group, previous_line, groups)
|
|
104
|
+
if local_variable_assignment?(statement)
|
|
105
|
+
current_group = handle_assignment(statement, current_group, previous_line, groups)
|
|
106
|
+
else
|
|
107
|
+
save_group_if_valid(groups, current_group)
|
|
108
|
+
current_group = []
|
|
109
|
+
end
|
|
110
|
+
[current_group, statement.loc.last_line]
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Handle an assignment statement.
|
|
114
|
+
#
|
|
115
|
+
# @param [RuboCop::AST::Node] statement The assignment statement.
|
|
116
|
+
# @param [Array<RuboCop::AST::Node>] current_group The current group.
|
|
117
|
+
# @param [Integer, nil] previous_line The previous line number.
|
|
118
|
+
# @param [Array<Array<RuboCop::AST::Node>>] groups The groups.
|
|
119
|
+
# @return [Array<RuboCop::AST::Node>] The updated current group.
|
|
120
|
+
def handle_assignment(statement, current_group, previous_line, groups)
|
|
121
|
+
if previous_line && statement.loc.line - previous_line > 1
|
|
122
|
+
save_group_if_valid(groups, current_group)
|
|
123
|
+
current_group = []
|
|
124
|
+
end
|
|
125
|
+
current_group << statement
|
|
126
|
+
current_group
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Save group if it has multiple assignments.
|
|
130
|
+
#
|
|
131
|
+
# @param [Array<Array<RuboCop::AST::Node>>] groups The groups.
|
|
132
|
+
# @param [Array<RuboCop::AST::Node>] group The group to potentially save.
|
|
133
|
+
# @return [void]
|
|
134
|
+
def save_group_if_valid(groups, group)
|
|
135
|
+
groups << group if group.size > 1
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Finalize groups by adding any remaining valid group.
|
|
139
|
+
#
|
|
140
|
+
# @param [Array<Array<RuboCop::AST::Node>>] groups The groups.
|
|
141
|
+
# @param [Array<RuboCop::AST::Node>] current_group The current group.
|
|
142
|
+
# @return [Array<Array<RuboCop::AST::Node>>] The finalized groups.
|
|
143
|
+
def finalize_groups(groups, current_group)
|
|
144
|
+
save_group_if_valid(groups, current_group)
|
|
145
|
+
groups
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Check if node is a local variable assignment.
|
|
149
|
+
#
|
|
150
|
+
# @param [RuboCop::AST::Node] node The node.
|
|
151
|
+
# @return [Boolean]
|
|
152
|
+
def local_variable_assignment?(node)
|
|
153
|
+
node.lvasgn_type?
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Check alignment for a group of assignments.
|
|
157
|
+
#
|
|
158
|
+
# @param [Array<RuboCop::AST::Node>] group The assignment group.
|
|
159
|
+
# @return [void]
|
|
160
|
+
def check_group_alignment(group)
|
|
161
|
+
columns = group.map { |asgn| asgn.loc.operator.column }
|
|
162
|
+
target_column = columns.max
|
|
163
|
+
|
|
164
|
+
group.each do |asgn|
|
|
165
|
+
current_column = asgn.loc.operator.column
|
|
166
|
+
next if current_column == target_column
|
|
167
|
+
|
|
168
|
+
add_offense(asgn.loc.name) do |corrector|
|
|
169
|
+
autocorrect_alignment(corrector, asgn, target_column)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Auto-correct the alignment of an assignment.
|
|
175
|
+
#
|
|
176
|
+
# @param [RuboCop::AST::Corrector] corrector The corrector.
|
|
177
|
+
# @param [RuboCop::AST::Node] asgn The assignment node.
|
|
178
|
+
# @param [Integer] target_column The target column for alignment.
|
|
179
|
+
# @return [void]
|
|
180
|
+
def autocorrect_alignment(corrector, asgn, target_column)
|
|
181
|
+
variable_name_end = asgn.loc.name.end_pos
|
|
182
|
+
operator_start = asgn.loc.operator.begin_pos
|
|
183
|
+
total_spaces = calculate_total_spaces(asgn, target_column)
|
|
184
|
+
|
|
185
|
+
corrector.replace(
|
|
186
|
+
range_between(variable_name_end, operator_start),
|
|
187
|
+
" " * total_spaces
|
|
188
|
+
)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Calculate total spaces needed for alignment.
|
|
192
|
+
#
|
|
193
|
+
# @param [RuboCop::AST::Node] asgn The assignment node.
|
|
194
|
+
# @param [Integer] target_column The target column for alignment.
|
|
195
|
+
# @return [Integer] The number of spaces (minimum 1).
|
|
196
|
+
def calculate_total_spaces(asgn, target_column)
|
|
197
|
+
current_column = asgn.loc.operator.column
|
|
198
|
+
current_spaces = asgn.loc.operator.begin_pos - asgn.loc.name.end_pos
|
|
199
|
+
spaces_needed = target_column - current_column
|
|
200
|
+
|
|
201
|
+
[1, current_spaces + spaces_needed].max
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Create a source range between two positions.
|
|
205
|
+
#
|
|
206
|
+
# @param [Integer] start_pos The start position.
|
|
207
|
+
# @param [Integer] end_pos The end position.
|
|
208
|
+
# @return [Parser::Source::Range]
|
|
209
|
+
def range_between(start_pos, end_pos)
|
|
210
|
+
Parser::Source::Range.new(
|
|
211
|
+
processed_source.buffer,
|
|
212
|
+
start_pos,
|
|
213
|
+
end_pos
|
|
214
|
+
)
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Vibe
|
|
6
|
+
# Enforces consistent ordering of describe blocks in RSpec files.
|
|
7
|
+
#
|
|
8
|
+
# Universal order: "class" → "constants" → ".class_method" → "#instance_method"
|
|
9
|
+
# Models add: "associations" and "validations" between "constants" and methods.
|
|
10
|
+
# Controllers add: RESTful actions between "constants" and methods.
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
# # bad
|
|
14
|
+
# describe User do
|
|
15
|
+
# describe "#name" do
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
# describe "class" do
|
|
19
|
+
# end
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
# # good (universal)
|
|
23
|
+
# describe Service do
|
|
24
|
+
# describe "class" do
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
# describe ".call" do
|
|
28
|
+
# end
|
|
29
|
+
#
|
|
30
|
+
# describe "#process" do
|
|
31
|
+
# end
|
|
32
|
+
# end
|
|
33
|
+
#
|
|
34
|
+
# # good (model with custom sections)
|
|
35
|
+
# describe User do
|
|
36
|
+
# describe "class" do
|
|
37
|
+
# end
|
|
38
|
+
#
|
|
39
|
+
# describe "associations" do
|
|
40
|
+
# end
|
|
41
|
+
#
|
|
42
|
+
# describe "validations" do
|
|
43
|
+
# end
|
|
44
|
+
#
|
|
45
|
+
# describe ".find_active" do
|
|
46
|
+
# end
|
|
47
|
+
#
|
|
48
|
+
# describe "#name" do
|
|
49
|
+
# end
|
|
50
|
+
# end
|
|
51
|
+
class DescribeBlockOrder < Base
|
|
52
|
+
extend AutoCorrector
|
|
53
|
+
include SpecFileHelper
|
|
54
|
+
|
|
55
|
+
MSG = "Describe blocks should be ordered: class → constants → .class_method → #instance_method."
|
|
56
|
+
|
|
57
|
+
# Priority for non-special, non-method descriptions (e.g., "callbacks", "scopes")
|
|
58
|
+
NON_SPECIAL_DESCRIPTION_PRIORITY = 300
|
|
59
|
+
# Default priority for descriptions that can't be categorized (e.g., constants, variables)
|
|
60
|
+
DEFAULT_PRIORITY = 999
|
|
61
|
+
|
|
62
|
+
MODEL_ORDER = %w(class associations validations).freeze
|
|
63
|
+
CONTROLLER_ACTIONS = %w(index show new create edit update destroy).freeze
|
|
64
|
+
SPECIAL_SECTIONS = {
|
|
65
|
+
"class" => 0,
|
|
66
|
+
"constants" => 5,
|
|
67
|
+
"associations" => 10,
|
|
68
|
+
"validations" => 20
|
|
69
|
+
}.freeze
|
|
70
|
+
|
|
71
|
+
# @!method describe_block?(node)
|
|
72
|
+
# Check if node is a describe block (matches both `describe` and `RSpec.describe`).
|
|
73
|
+
def_node_matcher :describe_block?, <<~PATTERN
|
|
74
|
+
(block (send _ :describe ...) ...)
|
|
75
|
+
PATTERN
|
|
76
|
+
|
|
77
|
+
# Check block nodes for describe calls.
|
|
78
|
+
#
|
|
79
|
+
# @param [RuboCop::AST::Node] node The block node.
|
|
80
|
+
# @return [void]
|
|
81
|
+
def on_block(node)
|
|
82
|
+
return unless spec_file?
|
|
83
|
+
return unless top_level_describe?(node)
|
|
84
|
+
|
|
85
|
+
describe_blocks = extract_describe_blocks(node)
|
|
86
|
+
return if describe_blocks.size < 2
|
|
87
|
+
|
|
88
|
+
violations = find_ordering_violations(describe_blocks)
|
|
89
|
+
violations.each do |block_info|
|
|
90
|
+
add_offense(block_info[:node]) do |corrector|
|
|
91
|
+
autocorrect(corrector, describe_blocks)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
alias on_numblock on_block
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
# Check if this is a top-level describe block.
|
|
100
|
+
#
|
|
101
|
+
# @param [RuboCop::AST::Node] node The block node.
|
|
102
|
+
# @return [Boolean]
|
|
103
|
+
def top_level_describe?(node)
|
|
104
|
+
describe_block?(node) &&
|
|
105
|
+
node.each_ancestor(:block).none? { |ancestor| describe_block?(ancestor) }
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Extract second-level describe blocks.
|
|
109
|
+
#
|
|
110
|
+
# @param [RuboCop::AST::Node] top_node The top-level describe block.
|
|
111
|
+
# @return [Array<Hash>] Array of describe block info.
|
|
112
|
+
def extract_describe_blocks(top_node)
|
|
113
|
+
if top_node.body
|
|
114
|
+
block_nodes(top_node).map.with_index { |node, index| build_block_info(node, index) }
|
|
115
|
+
else
|
|
116
|
+
[]
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Get block nodes from the top-level describe.
|
|
121
|
+
#
|
|
122
|
+
# @param [RuboCop::AST::Node] top_node The top-level describe block.
|
|
123
|
+
# @return [Array<RuboCop::AST::Node>]
|
|
124
|
+
def block_nodes(top_node)
|
|
125
|
+
children = top_node.body.begin_type? ? top_node.body.children : [top_node.body]
|
|
126
|
+
|
|
127
|
+
children.select { |child| child.block_type? && child.method?(:describe) }
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Build block information hash.
|
|
131
|
+
#
|
|
132
|
+
# @param [RuboCop::AST::Node] node The block node.
|
|
133
|
+
# @param [Integer] index The original index.
|
|
134
|
+
# @return [Hash] Block information.
|
|
135
|
+
def build_block_info(node, index)
|
|
136
|
+
description = extract_description(node)
|
|
137
|
+
|
|
138
|
+
{
|
|
139
|
+
node: node,
|
|
140
|
+
original_index: index,
|
|
141
|
+
description: description,
|
|
142
|
+
priority: categorize_description(description)
|
|
143
|
+
}
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Extract description string from describe block.
|
|
147
|
+
#
|
|
148
|
+
# Only string and symbol literals are supported for ordering.
|
|
149
|
+
# Constants and variables return nil and will be assigned DEFAULT_PRIORITY.
|
|
150
|
+
#
|
|
151
|
+
# @param [RuboCop::AST::Node] node The describe block node.
|
|
152
|
+
# @return [String] The description string from a string or symbol literal.
|
|
153
|
+
# @return [nil] When description is not a string/symbol literal.
|
|
154
|
+
def extract_description(node)
|
|
155
|
+
first_arg = node.send_node.first_argument
|
|
156
|
+
return unless first_arg
|
|
157
|
+
|
|
158
|
+
if first_arg.str_type?
|
|
159
|
+
first_arg.value
|
|
160
|
+
elsif first_arg.sym_type?
|
|
161
|
+
first_arg.value.to_s
|
|
162
|
+
# Intentionally returns nil for constants/variables.
|
|
163
|
+
# These will get DEFAULT_PRIORITY.
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Categorize description and assign priority.
|
|
168
|
+
#
|
|
169
|
+
# Universal order: class (0) → constants (5) → .class_method (100) → #instance_method (200)
|
|
170
|
+
# Models insert: associations (10), validations (20) between class and methods.
|
|
171
|
+
# Controllers insert: RESTful actions (30-36) between class and methods.
|
|
172
|
+
# Non-special descriptions get NON_SPECIAL_DESCRIPTION_PRIORITY (300).
|
|
173
|
+
# nil descriptions get DEFAULT_PRIORITY (999).
|
|
174
|
+
#
|
|
175
|
+
# @param [String, nil] description The describe block description.
|
|
176
|
+
# @return [Integer] Priority number (lower = earlier).
|
|
177
|
+
def categorize_description(description)
|
|
178
|
+
if description
|
|
179
|
+
special_section_priority(description) ||
|
|
180
|
+
controller_action_priority(description) ||
|
|
181
|
+
method_priority(description) ||
|
|
182
|
+
NON_SPECIAL_DESCRIPTION_PRIORITY
|
|
183
|
+
else
|
|
184
|
+
DEFAULT_PRIORITY
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Get priority for special sections.
|
|
189
|
+
#
|
|
190
|
+
# @param [String] description The describe block description.
|
|
191
|
+
# @return [Integer]
|
|
192
|
+
# @return [nil] When not a special section.
|
|
193
|
+
def special_section_priority(description)
|
|
194
|
+
SPECIAL_SECTIONS[description]
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Get priority for controller actions.
|
|
198
|
+
#
|
|
199
|
+
# @param [String] description The describe block description.
|
|
200
|
+
# @return [Integer]
|
|
201
|
+
# @return [nil] When not a controller action.
|
|
202
|
+
def controller_action_priority(description)
|
|
203
|
+
# Strip the # prefix for controller actions.
|
|
204
|
+
action_name = description.start_with?("#") ? description[1..] : description
|
|
205
|
+
if controller_action?(action_name)
|
|
206
|
+
30 + CONTROLLER_ACTIONS.index(action_name)
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Get priority for method descriptions.
|
|
211
|
+
#
|
|
212
|
+
# @param [String] description The describe block description.
|
|
213
|
+
# @return [Integer]
|
|
214
|
+
# @return [nil] When not a method description.
|
|
215
|
+
def method_priority(description)
|
|
216
|
+
return 100 if description.start_with?(".")
|
|
217
|
+
return 200 if description.start_with?("#")
|
|
218
|
+
|
|
219
|
+
nil
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Check if description is a controller action.
|
|
223
|
+
#
|
|
224
|
+
# @param [String] description The describe block description.
|
|
225
|
+
# @return [Boolean]
|
|
226
|
+
def controller_action?(description)
|
|
227
|
+
CONTROLLER_ACTIONS.include?(description)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Find describe blocks that are out of order.
|
|
231
|
+
#
|
|
232
|
+
# @param [Array<Hash>] blocks The list of describe blocks.
|
|
233
|
+
# @return [Array<Hash>] Blocks that violate ordering.
|
|
234
|
+
def find_ordering_violations(blocks)
|
|
235
|
+
violations = []
|
|
236
|
+
|
|
237
|
+
blocks.each_cons(2) do |current, following|
|
|
238
|
+
violations << following if current[:priority] > following[:priority]
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
violations.uniq
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Auto-correct by reordering describe blocks.
|
|
245
|
+
#
|
|
246
|
+
# @param [RuboCop::AST::Corrector] corrector The corrector.
|
|
247
|
+
# @param [Array<Hash>] blocks The list of describe blocks.
|
|
248
|
+
# @return [void]
|
|
249
|
+
def autocorrect(corrector, blocks)
|
|
250
|
+
sorted_blocks = blocks.sort_by { |b| [b[:priority], b[:original_index]] }
|
|
251
|
+
|
|
252
|
+
blocks.each_with_index do |block, index|
|
|
253
|
+
sorted_block = sorted_blocks[index]
|
|
254
|
+
next if block == sorted_block
|
|
255
|
+
|
|
256
|
+
corrector.replace(block[:node], sorted_block[:node].source)
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Vibe
|
|
6
|
+
# Enforces that `is_expected` is only used in one-liner `it { }` blocks.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# # bad
|
|
10
|
+
# it "returns true" do
|
|
11
|
+
# is_expected.to be(true)
|
|
12
|
+
# end
|
|
13
|
+
#
|
|
14
|
+
# # good
|
|
15
|
+
# it { is_expected.to be(true) }
|
|
16
|
+
#
|
|
17
|
+
# # good - expect with description is allowed
|
|
18
|
+
# it "returns the user" do
|
|
19
|
+
# expect(result).to eq(user)
|
|
20
|
+
# end
|
|
21
|
+
class IsExpectedOneLiner < Base
|
|
22
|
+
extend AutoCorrector
|
|
23
|
+
include SpecFileHelper
|
|
24
|
+
|
|
25
|
+
MSG = "Use one-liner `it { is_expected.to ... }` syntax when using `is_expected`."
|
|
26
|
+
|
|
27
|
+
# @!method is_expected_call?(node)
|
|
28
|
+
# Check if node is an is_expected call.
|
|
29
|
+
def_node_matcher :is_expected_call?, <<~PATTERN
|
|
30
|
+
(send nil? :is_expected)
|
|
31
|
+
PATTERN
|
|
32
|
+
|
|
33
|
+
# @!method example_block_with_description?(node)
|
|
34
|
+
# Check if block is an example block with a description.
|
|
35
|
+
def_node_matcher :example_block_with_description?, <<~PATTERN
|
|
36
|
+
(block (send nil? {:it :specify} (str _) ...) ...)
|
|
37
|
+
PATTERN
|
|
38
|
+
|
|
39
|
+
# Check send nodes for is_expected calls inside described blocks.
|
|
40
|
+
#
|
|
41
|
+
# @param [RuboCop::AST::Node] node The send node.
|
|
42
|
+
# @return [void]
|
|
43
|
+
def on_send(node)
|
|
44
|
+
return unless spec_file?
|
|
45
|
+
return unless is_expected_call?(node)
|
|
46
|
+
|
|
47
|
+
example_block = find_example_block(node)
|
|
48
|
+
return unless example_block
|
|
49
|
+
return unless example_block_with_description?(example_block)
|
|
50
|
+
return if complex_expectation?(example_block)
|
|
51
|
+
|
|
52
|
+
add_offense(example_block.send_node) do |corrector|
|
|
53
|
+
autocorrect(corrector, example_block)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
alias on_csend on_send
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
# Find the enclosing example block for a node.
|
|
61
|
+
#
|
|
62
|
+
# @param [RuboCop::AST::Node] node The node to search from.
|
|
63
|
+
# @return [RuboCop::AST::Node]
|
|
64
|
+
# @return [nil] When no example block is found.
|
|
65
|
+
def find_example_block(node)
|
|
66
|
+
node.each_ancestor(:block).find do |ancestor|
|
|
67
|
+
send_node = ancestor.send_node
|
|
68
|
+
send_node.method?(:it) || send_node.method?(:specify)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Check if the expectation is too complex for one-liner conversion.
|
|
73
|
+
#
|
|
74
|
+
# @param [RuboCop::AST::Node] node The block node.
|
|
75
|
+
# @return [Boolean]
|
|
76
|
+
def complex_expectation?(node)
|
|
77
|
+
expectation_source = node.body.source
|
|
78
|
+
|
|
79
|
+
# Multi-line expectations are complex.
|
|
80
|
+
return true if expectation_source.include?("\n")
|
|
81
|
+
|
|
82
|
+
# Expectations with compound matchers (.and, .or) are complex.
|
|
83
|
+
compound_matcher?(node.body)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Check if the expectation uses compound matchers.
|
|
87
|
+
#
|
|
88
|
+
# @param [RuboCop::AST::Node] node The expectation node.
|
|
89
|
+
# @return [Boolean]
|
|
90
|
+
def compound_matcher?(node)
|
|
91
|
+
node.each_descendant(:send).any? do |send_node|
|
|
92
|
+
send_node.method?(:and) || send_node.method?(:or)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Autocorrect the offense by converting to one-liner syntax.
|
|
97
|
+
#
|
|
98
|
+
# @param [RuboCop::Cop::Corrector] corrector The corrector.
|
|
99
|
+
# @param [RuboCop::AST::Node] node The block node.
|
|
100
|
+
# @return [void]
|
|
101
|
+
def autocorrect(corrector, node)
|
|
102
|
+
expectation_source = node.body.source
|
|
103
|
+
corrector.replace(node, "it { #{expectation_source} }")
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Vibe
|
|
6
|
+
module SpecFileHelper
|
|
7
|
+
SPEC_FILE_PATTERN = %r{spec/.*_spec\.rb}
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
# Check if file is a spec file.
|
|
12
|
+
#
|
|
13
|
+
# @return [Boolean]
|
|
14
|
+
def spec_file?
|
|
15
|
+
processed_source.file_path.match?(SPEC_FILE_PATTERN)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Vibe
|
|
6
|
+
# Enforces that controller specs only test assignment identity, not attributes or associations.
|
|
7
|
+
#
|
|
8
|
+
# Testing model attributes or associations from assigns is considered bad practice
|
|
9
|
+
# because it couples the controller spec to the model implementation.
|
|
10
|
+
# Controller specs should only verify that the correct object is assigned,
|
|
11
|
+
# not test the object's internal state or associations.
|
|
12
|
+
#
|
|
13
|
+
# @example
|
|
14
|
+
# # bad
|
|
15
|
+
# expect(assigns(:user).email).to eq('test@example.com')
|
|
16
|
+
# expect(assigns(:user).posts.count).to eq(5)
|
|
17
|
+
# expect(assigns(:post).author.name).to eq('John')
|
|
18
|
+
#
|
|
19
|
+
# # good
|
|
20
|
+
# expect(assigns(:user)).to eq(user)
|
|
21
|
+
# expect(assigns(:post)).to eq(post)
|
|
22
|
+
#
|
|
23
|
+
class NoAssignsAttributeTesting < Base
|
|
24
|
+
include SpecFileHelper
|
|
25
|
+
|
|
26
|
+
MSG = "Do not test attributes or associations from assigns. " \
|
|
27
|
+
"Only test the assignment itself: `expect(assigns(:var)).to eq(object)`"
|
|
28
|
+
|
|
29
|
+
CONTROLLER_SPEC_PATTERN = %r{spec/controllers/.*_spec\.rb}
|
|
30
|
+
|
|
31
|
+
# Matches assigns(:variable).method_call patterns.
|
|
32
|
+
# @!method assigns_with_method?(node)
|
|
33
|
+
def_node_matcher :assigns_with_method?, <<~PATTERN
|
|
34
|
+
(send
|
|
35
|
+
(send nil? :assigns ...)
|
|
36
|
+
$_)
|
|
37
|
+
PATTERN
|
|
38
|
+
|
|
39
|
+
# Checks if assigns is being called with a method in controller specs.
|
|
40
|
+
#
|
|
41
|
+
# @param node [RuboCop::AST::SendNode] the node being checked.
|
|
42
|
+
# @return [void]
|
|
43
|
+
def on_send(node)
|
|
44
|
+
return unless controller_spec_file?
|
|
45
|
+
return unless assigns_with_method?(node)
|
|
46
|
+
|
|
47
|
+
add_offense(node.loc.selector)
|
|
48
|
+
end
|
|
49
|
+
alias on_csend on_send
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
# Checks if the current file is a controller spec.
|
|
54
|
+
#
|
|
55
|
+
# @return [Boolean] true if the file is a controller spec.
|
|
56
|
+
def controller_spec_file?
|
|
57
|
+
processed_source.file_path.match?(CONTROLLER_SPEC_PATTERN)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|