rubocop-vibe 0.2.0 → 0.3.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 +4 -4
- data/config/default.yml +39 -4
- data/lib/rubocop/cop/vibe/blank_line_after_assignment.rb +217 -0
- data/lib/rubocop/cop/vibe/blank_line_before_expectation.rb +6 -1
- data/lib/rubocop/cop/vibe/class_organization.rb +20 -1
- data/lib/rubocop/cop/vibe/consecutive_assignment_alignment.rb +5 -102
- data/lib/rubocop/cop/vibe/consecutive_constant_alignment.rb +5 -113
- data/lib/rubocop/cop/vibe/consecutive_indexed_assignment_alignment.rb +145 -0
- data/lib/rubocop/cop/vibe/consecutive_let_alignment.rb +5 -94
- data/lib/rubocop/cop/vibe/describe_block_order.rb +6 -2
- data/lib/rubocop/cop/vibe/explicit_return_conditional.rb +192 -0
- data/lib/rubocop/cop/vibe/is_expected_one_liner.rb +3 -0
- data/lib/rubocop/cop/vibe/mixin/alignment_helpers.rb +92 -0
- data/lib/rubocop/cop/vibe/multiline_hash_argument_style.rb +171 -0
- data/lib/rubocop/cop/vibe/no_compound_conditions.rb +138 -0
- data/lib/rubocop/cop/vibe/no_rubocop_disable.rb +3 -0
- data/lib/rubocop/cop/vibe/no_skipped_tests.rb +1 -0
- data/lib/rubocop/cop/vibe/no_unless_guard_clause.rb +4 -0
- data/lib/rubocop/cop/vibe/prefer_one_liner_expectation.rb +4 -0
- data/lib/rubocop/cop/vibe/raise_unless_block.rb +1 -0
- data/lib/rubocop/cop/vibe/rspec_before_block_style.rb +114 -0
- data/lib/rubocop/cop/vibe/rspec_stub_chain_style.rb +4 -0
- data/lib/rubocop/cop/vibe_cops.rb +7 -0
- data/lib/rubocop/vibe/plugin.rb +4 -4
- data/lib/rubocop/vibe/version.rb +1 -1
- metadata +9 -2
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Vibe
|
|
6
|
+
# Shared helper methods for consecutive alignment cops.
|
|
7
|
+
#
|
|
8
|
+
# Provides common functionality for grouping consecutive statements
|
|
9
|
+
# and creating source ranges for autocorrection.
|
|
10
|
+
module AlignmentHelpers
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
# Extract statements from a body node.
|
|
14
|
+
#
|
|
15
|
+
# @param [RuboCop::AST::Node] body The body node.
|
|
16
|
+
# @return [Array<RuboCop::AST::Node>]
|
|
17
|
+
def extract_statements(body)
|
|
18
|
+
if body.begin_type?
|
|
19
|
+
body.children
|
|
20
|
+
else
|
|
21
|
+
[body]
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Group consecutive statements based on a predicate.
|
|
26
|
+
#
|
|
27
|
+
# Iterates through statements and groups consecutive ones where the block
|
|
28
|
+
# returns true. Groups are broken by non-matching statements or blank lines.
|
|
29
|
+
#
|
|
30
|
+
# @param [Array<RuboCop::AST::Node>] statements The statements.
|
|
31
|
+
# @yield [RuboCop::AST::Node] Block to determine if statement matches criteria.
|
|
32
|
+
# @return [Array<Array<RuboCop::AST::Node>>] Groups of consecutive matching statements.
|
|
33
|
+
def group_consecutive_statements(statements, &)
|
|
34
|
+
matching_with_indices = find_matching_statements(statements, &)
|
|
35
|
+
|
|
36
|
+
group_by_consecutive_lines(matching_with_indices, statements)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Find statements matching the predicate with their indices.
|
|
40
|
+
#
|
|
41
|
+
# @param [Array<RuboCop::AST::Node>] statements The statements.
|
|
42
|
+
# @return [Array<Array>] Array of [index, statement] pairs.
|
|
43
|
+
def find_matching_statements(statements)
|
|
44
|
+
statements.each_with_index.filter_map do |stmt, idx|
|
|
45
|
+
[idx, stmt] if yield(stmt)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Group matched statements that are on consecutive lines.
|
|
50
|
+
#
|
|
51
|
+
# @param [Array<Array>] matches Array of [index, statement] pairs.
|
|
52
|
+
# @param [Array<RuboCop::AST::Node>] statements Original statements for line lookups.
|
|
53
|
+
# @return [Array<Array<RuboCop::AST::Node>>] Groups with 2+ consecutive statements.
|
|
54
|
+
def group_by_consecutive_lines(matches, statements)
|
|
55
|
+
matches
|
|
56
|
+
.chunk_while { |a, b| consecutive?(a, b, statements) }
|
|
57
|
+
.filter_map { |group| group.map(&:last) if group.size > 1 }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Check if two matched statements are consecutive (no gaps).
|
|
61
|
+
#
|
|
62
|
+
# @param [Array] first First [index, statement] pair.
|
|
63
|
+
# @param [Array] second Second [index, statement] pair.
|
|
64
|
+
# @param [Array<RuboCop::AST::Node>] statements Original statements.
|
|
65
|
+
# @return [Boolean]
|
|
66
|
+
def consecutive?(first, second, statements)
|
|
67
|
+
idx_a, stmt_a = first
|
|
68
|
+
idx_b, = second
|
|
69
|
+
idx_b == idx_a + 1 && no_blank_line_between?(stmt_a, statements[idx_b])
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Check if there's no blank line between two statements.
|
|
73
|
+
#
|
|
74
|
+
# @param [RuboCop::AST::Node] stmt_a First statement.
|
|
75
|
+
# @param [RuboCop::AST::Node] stmt_b Second statement.
|
|
76
|
+
# @return [Boolean]
|
|
77
|
+
def no_blank_line_between?(stmt_a, stmt_b)
|
|
78
|
+
stmt_b.loc.line - stmt_a.loc.last_line <= 1
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Create a source range between two positions.
|
|
82
|
+
#
|
|
83
|
+
# @param [Integer] start_pos The start position.
|
|
84
|
+
# @param [Integer] end_pos The end position.
|
|
85
|
+
# @return [Parser::Source::Range]
|
|
86
|
+
def range_between(start_pos, end_pos)
|
|
87
|
+
Parser::Source::Range.new(processed_source.buffer, start_pos, end_pos)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Vibe
|
|
6
|
+
# Enforces that hash arguments in multiline method calls are on separate
|
|
7
|
+
# lines and alphabetically ordered by key name.
|
|
8
|
+
#
|
|
9
|
+
# This cop applies to method calls where the closing parenthesis is on
|
|
10
|
+
# its own line.
|
|
11
|
+
#
|
|
12
|
+
# Value alignment (table style) is handled separately by Layout/HashAlignment.
|
|
13
|
+
#
|
|
14
|
+
# @example
|
|
15
|
+
# # bad - multiple hash pairs on same line
|
|
16
|
+
# SomeService.call(
|
|
17
|
+
# website_id: website.id, data: data
|
|
18
|
+
# )
|
|
19
|
+
#
|
|
20
|
+
# # bad - hash pairs not alphabetically ordered
|
|
21
|
+
# SomeService.call(
|
|
22
|
+
# website_id: website.id,
|
|
23
|
+
# data: data
|
|
24
|
+
# )
|
|
25
|
+
#
|
|
26
|
+
# # good
|
|
27
|
+
# SomeService.call(
|
|
28
|
+
# data: data,
|
|
29
|
+
# website_id: website.id
|
|
30
|
+
# )
|
|
31
|
+
class MultilineHashArgumentStyle < Base
|
|
32
|
+
extend AutoCorrector
|
|
33
|
+
|
|
34
|
+
MSG = "Hash arguments in multiline calls should be one per line " \
|
|
35
|
+
"and alphabetically ordered."
|
|
36
|
+
|
|
37
|
+
# Check send nodes for hash arguments that need reformatting.
|
|
38
|
+
#
|
|
39
|
+
# @param [RuboCop::AST::Node] node The send node.
|
|
40
|
+
# @return [void]
|
|
41
|
+
def on_send(node)
|
|
42
|
+
return unless multiline_call_with_hash?(node)
|
|
43
|
+
|
|
44
|
+
hash_arg = find_hash_argument(node)
|
|
45
|
+
|
|
46
|
+
return unless hash_arg
|
|
47
|
+
return unless needs_correction?(hash_arg)
|
|
48
|
+
|
|
49
|
+
add_offense(hash_arg) do |corrector|
|
|
50
|
+
autocorrect(corrector, hash_arg)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
alias on_csend on_send
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
# Check if this is a multiline call with closing paren on own line.
|
|
58
|
+
#
|
|
59
|
+
# @param [RuboCop::AST::Node] node The send node.
|
|
60
|
+
# @return [Boolean]
|
|
61
|
+
def multiline_call_with_hash?(node)
|
|
62
|
+
node.parenthesized? && closing_paren_on_own_line?(node)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Check if closing paren is on its own line (after last argument).
|
|
66
|
+
#
|
|
67
|
+
# @param [RuboCop::AST::Node] node The send node.
|
|
68
|
+
# @return [Boolean]
|
|
69
|
+
def closing_paren_on_own_line?(node)
|
|
70
|
+
last_arg = node.last_argument
|
|
71
|
+
|
|
72
|
+
if last_arg
|
|
73
|
+
node.loc.end.line > last_arg.loc.last_line
|
|
74
|
+
else
|
|
75
|
+
false
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Find hash argument in the call (if any).
|
|
80
|
+
#
|
|
81
|
+
# @param [RuboCop::AST::Node] node The send node.
|
|
82
|
+
# @return [RuboCop::AST::Node, nil]
|
|
83
|
+
def find_hash_argument(node)
|
|
84
|
+
node.arguments.find(&:hash_type?)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Check if hash needs correction (same line or unordered).
|
|
88
|
+
#
|
|
89
|
+
# @param [RuboCop::AST::Node] hash_arg The hash node.
|
|
90
|
+
# @return [Boolean]
|
|
91
|
+
def needs_correction?(hash_arg)
|
|
92
|
+
pairs = hash_arg.pairs
|
|
93
|
+
|
|
94
|
+
return false if pairs.size < 2
|
|
95
|
+
|
|
96
|
+
multiple_pairs_on_same_line?(pairs) || !pairs_alphabetically_ordered?(pairs)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Check if multiple pairs are on the same line.
|
|
100
|
+
#
|
|
101
|
+
# @param [Array<RuboCop::AST::Node>] pairs The hash pairs.
|
|
102
|
+
# @return [Boolean]
|
|
103
|
+
def multiple_pairs_on_same_line?(pairs)
|
|
104
|
+
lines = pairs.map { |pair| pair.loc.line }
|
|
105
|
+
|
|
106
|
+
lines.size != lines.uniq.size
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Check if pairs are alphabetically ordered by key name.
|
|
110
|
+
#
|
|
111
|
+
# @param [Array<RuboCop::AST::Node>] pairs The hash pairs.
|
|
112
|
+
# @return [Boolean]
|
|
113
|
+
def pairs_alphabetically_ordered?(pairs)
|
|
114
|
+
key_names = pairs.map { |pair| extract_key_name(pair) }
|
|
115
|
+
|
|
116
|
+
key_names == key_names.sort
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Extract the key name as a string for sorting.
|
|
120
|
+
#
|
|
121
|
+
# @param [RuboCop::AST::Node] pair The hash pair node.
|
|
122
|
+
# @return [String]
|
|
123
|
+
def extract_key_name(pair)
|
|
124
|
+
key = pair.key
|
|
125
|
+
|
|
126
|
+
if key.type?(:sym, :str)
|
|
127
|
+
key.value.to_s
|
|
128
|
+
else
|
|
129
|
+
key.source
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Autocorrect by reordering and splitting pairs.
|
|
134
|
+
#
|
|
135
|
+
# @param [RuboCop::Cop::Corrector] corrector The corrector.
|
|
136
|
+
# @param [RuboCop::AST::Node] hash_arg The hash node.
|
|
137
|
+
# @return [void]
|
|
138
|
+
def autocorrect(corrector, hash_arg)
|
|
139
|
+
pairs = hash_arg.pairs
|
|
140
|
+
sorted = pairs.sort_by { |pair| extract_key_name(pair) }
|
|
141
|
+
indentation = calculate_indentation(pairs.first)
|
|
142
|
+
replacement = build_replacement(sorted, indentation)
|
|
143
|
+
|
|
144
|
+
corrector.replace(hash_arg, replacement)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Calculate indentation for reformatted pairs.
|
|
148
|
+
#
|
|
149
|
+
# @param [RuboCop::AST::Node] first_pair The first pair node.
|
|
150
|
+
# @return [String]
|
|
151
|
+
def calculate_indentation(first_pair)
|
|
152
|
+
" " * first_pair.loc.column
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Build the replacement string with sorted pairs on separate lines.
|
|
156
|
+
#
|
|
157
|
+
# @param [Array<RuboCop::AST::Node>] sorted_pairs The sorted pairs.
|
|
158
|
+
# @param [String] indentation The indentation string.
|
|
159
|
+
# @return [String]
|
|
160
|
+
def build_replacement(sorted_pairs, indentation)
|
|
161
|
+
sorted_pairs.map.with_index do |pair, index|
|
|
162
|
+
prefix = index.zero? ? "" : indentation
|
|
163
|
+
suffix = index == sorted_pairs.size - 1 ? "" : ","
|
|
164
|
+
|
|
165
|
+
"#{prefix}#{pair.source}#{suffix}"
|
|
166
|
+
end.join("\n")
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Vibe
|
|
6
|
+
# Enforces extracting compound boolean conditions into named methods.
|
|
7
|
+
#
|
|
8
|
+
# When a conditional has multiple boolean expressions joined by `&&` or
|
|
9
|
+
# `||`, the intent becomes unclear. Extracting to a descriptively-named
|
|
10
|
+
# method documents the business logic.
|
|
11
|
+
#
|
|
12
|
+
# This cop does NOT flag compound conditions that are:
|
|
13
|
+
# - The implied return value of a method (the extraction target)
|
|
14
|
+
#
|
|
15
|
+
# This cop DOES flag compound conditions that are:
|
|
16
|
+
# - In if/unless/while/until/when/ternary conditions
|
|
17
|
+
# - Inside an explicit return statement
|
|
18
|
+
#
|
|
19
|
+
# @example
|
|
20
|
+
# # bad - multiple conditions obscure intent
|
|
21
|
+
# if user.active? && user.verified?
|
|
22
|
+
# grant_access
|
|
23
|
+
# end
|
|
24
|
+
#
|
|
25
|
+
# # bad - mixing operators
|
|
26
|
+
# return if admin? || moderator?
|
|
27
|
+
#
|
|
28
|
+
# # bad - negation wrapping compound
|
|
29
|
+
# if !(order.paid? && order.shipped?)
|
|
30
|
+
# send_reminder
|
|
31
|
+
# end
|
|
32
|
+
#
|
|
33
|
+
# # bad - explicit return with compound
|
|
34
|
+
# def can_participate?
|
|
35
|
+
# return admin? || moderator? if override?
|
|
36
|
+
# check_other_conditions
|
|
37
|
+
# end
|
|
38
|
+
#
|
|
39
|
+
# # good - extracted to named method
|
|
40
|
+
# if user.can_participate?
|
|
41
|
+
# grant_access
|
|
42
|
+
# end
|
|
43
|
+
#
|
|
44
|
+
# # good - compound as implied method return value (this is the extraction)
|
|
45
|
+
# def can_participate?
|
|
46
|
+
# user.active? && user.verified?
|
|
47
|
+
# end
|
|
48
|
+
class NoCompoundConditions < Base
|
|
49
|
+
MSG = "Extract compound conditions into a named method."
|
|
50
|
+
|
|
51
|
+
# Check and/or nodes for conditional context.
|
|
52
|
+
#
|
|
53
|
+
# @param [RuboCop::AST::Node] node The and/or node.
|
|
54
|
+
# @return [void]
|
|
55
|
+
def on_and(node)
|
|
56
|
+
check_compound(node)
|
|
57
|
+
end
|
|
58
|
+
alias on_or on_and
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
# Check if a compound condition should be flagged.
|
|
63
|
+
#
|
|
64
|
+
# @param [RuboCop::AST::Node] node The and/or node.
|
|
65
|
+
# @return [void]
|
|
66
|
+
def check_compound(node)
|
|
67
|
+
if inside_return_statement?(node) || in_conditional_position?(node)
|
|
68
|
+
add_offense(node)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Check if node is in a conditional position (if/unless/while/until/when condition).
|
|
73
|
+
#
|
|
74
|
+
# @param [RuboCop::AST::Node] node The and/or node.
|
|
75
|
+
# @return [Boolean]
|
|
76
|
+
def in_conditional_position?(node)
|
|
77
|
+
ancestor = conditional_ancestor(node)
|
|
78
|
+
|
|
79
|
+
if ancestor
|
|
80
|
+
condition_of_ancestor?(node, ancestor)
|
|
81
|
+
else
|
|
82
|
+
false
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Find the nearest conditional ancestor (if/while/until/when).
|
|
87
|
+
#
|
|
88
|
+
# @param [RuboCop::AST::Node] node The node.
|
|
89
|
+
# @return [RuboCop::AST::Node, nil]
|
|
90
|
+
def conditional_ancestor(node)
|
|
91
|
+
node.each_ancestor.find { |a| a.type?(:if, :while, :until, :when) }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Check if node is (part of) the condition of the ancestor.
|
|
95
|
+
#
|
|
96
|
+
# @param [RuboCop::AST::Node] node The and/or node.
|
|
97
|
+
# @param [RuboCop::AST::Node] ancestor The if/while/until/when ancestor.
|
|
98
|
+
# @return [Boolean]
|
|
99
|
+
def condition_of_ancestor?(node, ancestor)
|
|
100
|
+
condition = condition_node(ancestor)
|
|
101
|
+
|
|
102
|
+
node_within_condition?(node, condition)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Extract the condition node from a conditional ancestor.
|
|
106
|
+
#
|
|
107
|
+
# @param [RuboCop::AST::Node] ancestor The conditional ancestor.
|
|
108
|
+
# @return [RuboCop::AST::Node]
|
|
109
|
+
def condition_node(ancestor)
|
|
110
|
+
if ancestor.type?(:if, :while, :until)
|
|
111
|
+
ancestor.condition
|
|
112
|
+
else
|
|
113
|
+
ancestor
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Check if node is within the condition subtree.
|
|
118
|
+
#
|
|
119
|
+
# @param [RuboCop::AST::Node] node The node to find.
|
|
120
|
+
# @param [RuboCop::AST::Node] condition The condition root.
|
|
121
|
+
# @return [Boolean]
|
|
122
|
+
def node_within_condition?(node, condition)
|
|
123
|
+
return true if condition == node
|
|
124
|
+
|
|
125
|
+
condition.each_descendant.any?(node)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Check if node is inside a return statement.
|
|
129
|
+
#
|
|
130
|
+
# @param [RuboCop::AST::Node] node The node.
|
|
131
|
+
# @return [Boolean]
|
|
132
|
+
def inside_return_statement?(node)
|
|
133
|
+
node.each_ancestor.any?(&:return_type?)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
@@ -33,6 +33,9 @@ module RuboCop
|
|
|
33
33
|
# task :my_task do
|
|
34
34
|
# # ...
|
|
35
35
|
# end
|
|
36
|
+
#
|
|
37
|
+
# NOTE: Examples use underscore (rubocop_disable) to prevent RuboCop from
|
|
38
|
+
# parsing them as actual directives. In real code, use colon (rubocop:disable).
|
|
36
39
|
class NoRubocopDisable < Base
|
|
37
40
|
MSG = "Do not disable `%<cops>s`. Fix the issue or configure globally in `.rubocop.yml`."
|
|
38
41
|
MSG_NO_COP = "Do not use `# rubocop:disable`. Fix the issue or configure globally in `.rubocop.yml`."
|
|
@@ -98,6 +98,7 @@ module RuboCop
|
|
|
98
98
|
def following_statements?(parent, node)
|
|
99
99
|
siblings = parent.children
|
|
100
100
|
node_index = siblings.index(node)
|
|
101
|
+
|
|
101
102
|
siblings[(node_index + 1)..].any?
|
|
102
103
|
end
|
|
103
104
|
|
|
@@ -151,6 +152,7 @@ module RuboCop
|
|
|
151
152
|
def autocorrect(corrector, node)
|
|
152
153
|
replacement = build_replacement(node)
|
|
153
154
|
range = replacement_range(node)
|
|
155
|
+
|
|
154
156
|
corrector.replace(range, replacement)
|
|
155
157
|
end
|
|
156
158
|
|
|
@@ -212,6 +214,7 @@ module RuboCop
|
|
|
212
214
|
def get_remaining_code(node)
|
|
213
215
|
siblings = node.parent.children
|
|
214
216
|
node_index = siblings.index(node)
|
|
217
|
+
|
|
215
218
|
siblings[(node_index + 1)..].map(&:source).join("\n")
|
|
216
219
|
end
|
|
217
220
|
|
|
@@ -223,6 +226,7 @@ module RuboCop
|
|
|
223
226
|
siblings = node.parent.children
|
|
224
227
|
start_pos = node.source_range.begin_pos
|
|
225
228
|
end_pos = siblings.last.source_range.end_pos
|
|
229
|
+
|
|
226
230
|
Parser::Source::Range.new(node.source_range.source_buffer, start_pos, end_pos)
|
|
227
231
|
end
|
|
228
232
|
|
|
@@ -57,6 +57,7 @@ module RuboCop
|
|
|
57
57
|
return unless single_statement?(node.body)
|
|
58
58
|
|
|
59
59
|
expectation = extract_expectation(node.body)
|
|
60
|
+
|
|
60
61
|
return unless simple_expectation?(expectation)
|
|
61
62
|
return if complex_expectation?(node)
|
|
62
63
|
|
|
@@ -99,6 +100,7 @@ module RuboCop
|
|
|
99
100
|
# @return [void]
|
|
100
101
|
def autocorrect(corrector, node)
|
|
101
102
|
expectation_source = node.body.source
|
|
103
|
+
|
|
102
104
|
corrector.replace(node, "it { #{expectation_source} }")
|
|
103
105
|
end
|
|
104
106
|
|
|
@@ -141,6 +143,7 @@ module RuboCop
|
|
|
141
143
|
def find_expectation_in_chain(node)
|
|
142
144
|
# Traverse up the chain looking for expect or is_expected.
|
|
143
145
|
current = node
|
|
146
|
+
|
|
144
147
|
while current&.send_type?
|
|
145
148
|
return current if expectation_method?(current)
|
|
146
149
|
|
|
@@ -174,6 +177,7 @@ module RuboCop
|
|
|
174
177
|
# @return [Boolean]
|
|
175
178
|
def expect_subject?(node)
|
|
176
179
|
argument = node.first_argument
|
|
180
|
+
|
|
177
181
|
return false unless argument
|
|
178
182
|
return false unless argument.send_type?
|
|
179
183
|
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Vibe
|
|
6
|
+
# Enforces using `do...end` syntax instead of braces for RSpec `before` blocks.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# # bad
|
|
10
|
+
# before { setup_data }
|
|
11
|
+
#
|
|
12
|
+
# # bad
|
|
13
|
+
# before(:each) { setup_data }
|
|
14
|
+
#
|
|
15
|
+
# # good
|
|
16
|
+
# before do
|
|
17
|
+
# setup_data
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# # good
|
|
21
|
+
# before(:each) do
|
|
22
|
+
# setup_data
|
|
23
|
+
# end
|
|
24
|
+
class RspecBeforeBlockStyle < Base
|
|
25
|
+
extend AutoCorrector
|
|
26
|
+
include SpecFileHelper
|
|
27
|
+
|
|
28
|
+
MSG = "Use `do...end` block syntax instead of braces for `%<method>s` blocks."
|
|
29
|
+
|
|
30
|
+
HOOK_METHODS = %i(before after around).freeze
|
|
31
|
+
|
|
32
|
+
# Check block nodes for brace-style hooks.
|
|
33
|
+
#
|
|
34
|
+
# @param [RuboCop::AST::Node] node The block node.
|
|
35
|
+
# @return [void]
|
|
36
|
+
def on_block(node)
|
|
37
|
+
return unless spec_file?
|
|
38
|
+
return unless hook_block?(node)
|
|
39
|
+
return unless node.braces?
|
|
40
|
+
|
|
41
|
+
method_name = node.method_name
|
|
42
|
+
|
|
43
|
+
add_offense(node.loc.begin, message: format(MSG, method: method_name)) do |corrector|
|
|
44
|
+
autocorrect(corrector, node)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
alias on_numblock on_block
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
# Check if the block is a hook block (before, after, around).
|
|
52
|
+
#
|
|
53
|
+
# @param [RuboCop::AST::Node] node The block node.
|
|
54
|
+
# @return [Boolean]
|
|
55
|
+
def hook_block?(node)
|
|
56
|
+
send_node = node.send_node
|
|
57
|
+
|
|
58
|
+
send_node.receiver.nil? && HOOK_METHODS.include?(send_node.method_name)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Autocorrect the offense by converting braces to do...end.
|
|
62
|
+
#
|
|
63
|
+
# @param [RuboCop::Cop::Corrector] corrector The corrector.
|
|
64
|
+
# @param [RuboCop::AST::Node] node The block node.
|
|
65
|
+
# @return [void]
|
|
66
|
+
def autocorrect(corrector, node)
|
|
67
|
+
base_indent = " " * node.loc.column
|
|
68
|
+
replacement = build_replacement(node, base_indent)
|
|
69
|
+
|
|
70
|
+
corrector.replace(block_range(node), replacement)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Build the replacement string for the block.
|
|
74
|
+
#
|
|
75
|
+
# @param [RuboCop::AST::Node] node The block node.
|
|
76
|
+
# @param [String] base_indent The base indentation.
|
|
77
|
+
# @return [String]
|
|
78
|
+
def build_replacement(node, base_indent)
|
|
79
|
+
parts = ["do"]
|
|
80
|
+
|
|
81
|
+
if node.arguments.source_range
|
|
82
|
+
parts << " #{node.arguments.source}"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
if node.body
|
|
86
|
+
body_source = format_body(node.body, "#{base_indent} ")
|
|
87
|
+
parts << "\n#{body_source}"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
parts << "\n#{base_indent}end"
|
|
91
|
+
|
|
92
|
+
parts.join
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Get the range from the opening brace to the closing brace.
|
|
96
|
+
#
|
|
97
|
+
# @param [RuboCop::AST::Node] node The block node.
|
|
98
|
+
# @return [Parser::Source::Range]
|
|
99
|
+
def block_range(node)
|
|
100
|
+
node.loc.begin.join(node.loc.end)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Format the body with proper indentation.
|
|
104
|
+
#
|
|
105
|
+
# @param [RuboCop::AST::Node] body The body node.
|
|
106
|
+
# @param [String] indent The indentation string.
|
|
107
|
+
# @return [String]
|
|
108
|
+
def format_body(body, indent)
|
|
109
|
+
body.source.lines.map { |line| "#{indent}#{line.strip}" }.join("\n")
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -85,11 +85,13 @@ module RuboCop
|
|
|
85
85
|
def check_receive_chain(to_node, receive_chain)
|
|
86
86
|
chain = extract_chain(receive_chain)
|
|
87
87
|
receive_index = find_receive_index(chain)
|
|
88
|
+
|
|
88
89
|
return unless receive_index
|
|
89
90
|
return unless chain_has_with?(chain)
|
|
90
91
|
return unless line_exceeds_max_length?(to_node)
|
|
91
92
|
|
|
92
93
|
methods = chain[(receive_index + 1)..]
|
|
94
|
+
|
|
93
95
|
check_methods_alignment(to_node, chain[receive_index], methods)
|
|
94
96
|
end
|
|
95
97
|
|
|
@@ -170,6 +172,7 @@ module RuboCop
|
|
|
170
172
|
# @return [Boolean]
|
|
171
173
|
def should_flag?(previous_node, method_node, is_first)
|
|
172
174
|
reference_line = is_first ? previous_node.loc.last_line : previous_node.loc.selector.line
|
|
175
|
+
|
|
173
176
|
reference_line == method_node.loc.selector.line
|
|
174
177
|
end
|
|
175
178
|
|
|
@@ -223,6 +226,7 @@ module RuboCop
|
|
|
223
226
|
# @return [String]
|
|
224
227
|
def calculate_indentation(node)
|
|
225
228
|
base_column = find_chain_start_column(node)
|
|
229
|
+
|
|
226
230
|
" " * (base_column + 2)
|
|
227
231
|
end
|
|
228
232
|
|
|
@@ -1,19 +1,26 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "vibe/mixin/alignment_helpers"
|
|
3
4
|
require_relative "vibe/mixin/spec_file_helper"
|
|
4
5
|
|
|
6
|
+
require_relative "vibe/blank_line_after_assignment"
|
|
5
7
|
require_relative "vibe/blank_line_before_expectation"
|
|
6
8
|
require_relative "vibe/class_organization"
|
|
7
9
|
require_relative "vibe/consecutive_assignment_alignment"
|
|
8
10
|
require_relative "vibe/consecutive_constant_alignment"
|
|
11
|
+
require_relative "vibe/consecutive_indexed_assignment_alignment"
|
|
9
12
|
require_relative "vibe/consecutive_let_alignment"
|
|
10
13
|
require_relative "vibe/describe_block_order"
|
|
14
|
+
require_relative "vibe/explicit_return_conditional"
|
|
11
15
|
require_relative "vibe/is_expected_one_liner"
|
|
16
|
+
require_relative "vibe/multiline_hash_argument_style"
|
|
12
17
|
require_relative "vibe/no_assigns_attribute_testing"
|
|
18
|
+
require_relative "vibe/no_compound_conditions"
|
|
13
19
|
require_relative "vibe/no_rubocop_disable"
|
|
14
20
|
require_relative "vibe/no_skipped_tests"
|
|
15
21
|
require_relative "vibe/no_unless_guard_clause"
|
|
16
22
|
require_relative "vibe/prefer_one_liner_expectation"
|
|
17
23
|
require_relative "vibe/raise_unless_block"
|
|
24
|
+
require_relative "vibe/rspec_before_block_style"
|
|
18
25
|
require_relative "vibe/rspec_stub_chain_style"
|
|
19
26
|
require_relative "vibe/service_call_method"
|
data/lib/rubocop/vibe/plugin.rb
CHANGED
|
@@ -10,10 +10,10 @@ module RuboCop
|
|
|
10
10
|
# @return [LintRoller::About] Information about the plug-in.
|
|
11
11
|
def about
|
|
12
12
|
LintRoller::About.new(
|
|
13
|
-
|
|
14
|
-
version: VERSION,
|
|
13
|
+
description: "A set of custom cops to use on AI generated code.",
|
|
15
14
|
homepage: "https://github.com/tristandunn/rubocop-vibe",
|
|
16
|
-
|
|
15
|
+
name: "rubocop-vibe",
|
|
16
|
+
version: VERSION
|
|
17
17
|
)
|
|
18
18
|
end
|
|
19
19
|
|
|
@@ -23,8 +23,8 @@ module RuboCop
|
|
|
23
23
|
# @return [LintRoller::Rules] The rules for this plug-in.
|
|
24
24
|
def rules(_context)
|
|
25
25
|
LintRoller::Rules.new(
|
|
26
|
-
type: :path,
|
|
27
26
|
config_format: :rubocop,
|
|
27
|
+
type: :path,
|
|
28
28
|
value: Pathname.new(__dir__).join("../../../config/default.yml")
|
|
29
29
|
)
|
|
30
30
|
end
|