rubocop-vibe 0.1.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 +64 -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 +121 -0
- data/lib/rubocop/cop/vibe/consecutive_indexed_assignment_alignment.rb +145 -0
- data/lib/rubocop/cop/vibe/consecutive_let_alignment.rb +130 -0
- data/lib/rubocop/cop/vibe/describe_block_order.rb +7 -3
- 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 +66 -16
- 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 +102 -0
- data/lib/rubocop/cop/vibe/rspec_before_block_style.rb +114 -0
- data/lib/rubocop/cop/vibe/rspec_stub_chain_style.rb +260 -0
- data/lib/rubocop/cop/vibe_cops.rb +11 -0
- data/lib/rubocop/vibe/plugin.rb +4 -4
- data/lib/rubocop/vibe/version.rb +1 -1
- metadata +13 -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
|
|
@@ -9,30 +9,40 @@ module RuboCop
|
|
|
9
9
|
# per-line basis. If a cop needs to be disabled, it should be configured
|
|
10
10
|
# globally in `.rubocop.yml` with proper justification.
|
|
11
11
|
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
# end
|
|
12
|
+
# Specific cops can be allowed via the `AllowedCops` configuration option.
|
|
13
|
+
# If multiple cops are disabled in one comment, only the disallowed ones
|
|
14
|
+
# will be flagged.
|
|
16
15
|
#
|
|
17
|
-
# @example Bad -
|
|
18
|
-
# # rubocop_disable Style/SomeCop
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
16
|
+
# @example Bad - inline disable directive
|
|
17
|
+
# do_something # rubocop_disable Style/SomeCop
|
|
18
|
+
#
|
|
19
|
+
# @example Bad - disable without specifying a cop
|
|
20
|
+
# # rubocop_disable all
|
|
21
|
+
# do_something
|
|
23
22
|
#
|
|
24
23
|
# @example Good - fix the issue instead
|
|
25
|
-
#
|
|
26
|
-
# do_something_correctly
|
|
27
|
-
# end
|
|
24
|
+
# do_something_correctly
|
|
28
25
|
#
|
|
29
26
|
# @example Good - configure globally in .rubocop.yml
|
|
27
|
+
# # In .rubocop.yml:
|
|
30
28
|
# # Style/SomeCop:
|
|
31
29
|
# # Enabled: false
|
|
30
|
+
#
|
|
31
|
+
# @example AllowedCops: ['Rails/RakeEnvironment'] - allowed cop is not flagged
|
|
32
|
+
# # rubocop_disable Rails/RakeEnvironment
|
|
33
|
+
# task :my_task do
|
|
34
|
+
# # ...
|
|
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).
|
|
32
39
|
class NoRubocopDisable < Base
|
|
33
|
-
MSG
|
|
40
|
+
MSG = "Do not disable `%<cops>s`. Fix the issue or configure globally in `.rubocop.yml`."
|
|
41
|
+
MSG_NO_COP = "Do not use `# rubocop:disable`. Fix the issue or configure globally in `.rubocop.yml`."
|
|
34
42
|
|
|
35
|
-
DISABLE_PATTERN
|
|
43
|
+
DISABLE_PATTERN = /\A#\s*rubocop\s*:\s*disable\b/i
|
|
44
|
+
COP_NAME_PATTERN = %r{[A-Za-z]+/[A-Za-z0-9]+}
|
|
45
|
+
ALL_PATTERN = /\brubocop\s*:\s*disable\s+all\b/i
|
|
36
46
|
|
|
37
47
|
# Check for rubocop:disable comments.
|
|
38
48
|
#
|
|
@@ -41,7 +51,7 @@ module RuboCop
|
|
|
41
51
|
processed_source.comments.each do |comment|
|
|
42
52
|
next unless disable_comment?(comment)
|
|
43
53
|
|
|
44
|
-
|
|
54
|
+
check_disabled_cops(comment)
|
|
45
55
|
end
|
|
46
56
|
end
|
|
47
57
|
|
|
@@ -54,6 +64,46 @@ module RuboCop
|
|
|
54
64
|
def disable_comment?(comment)
|
|
55
65
|
DISABLE_PATTERN.match?(comment.text)
|
|
56
66
|
end
|
|
67
|
+
|
|
68
|
+
# Check disabled cops and flag disallowed ones.
|
|
69
|
+
#
|
|
70
|
+
# @param [Parser::Source::Comment] comment The comment to check.
|
|
71
|
+
# @return [void]
|
|
72
|
+
def check_disabled_cops(comment)
|
|
73
|
+
cops = extract_cop_names(comment.text)
|
|
74
|
+
|
|
75
|
+
if cops.empty?
|
|
76
|
+
add_offense(comment, message: MSG_NO_COP)
|
|
77
|
+
else
|
|
78
|
+
disallowed = cops.reject { |cop| allowed_cop?(cop) }
|
|
79
|
+
return if disallowed.empty?
|
|
80
|
+
|
|
81
|
+
add_offense(comment, message: format(MSG, cops: disallowed.join(", ")))
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Extract cop names from a rubocop:disable comment.
|
|
86
|
+
#
|
|
87
|
+
# @param [String] text The comment text.
|
|
88
|
+
# @return [Array<String>]
|
|
89
|
+
def extract_cop_names(text)
|
|
90
|
+
text.scan(COP_NAME_PATTERN)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Check if a cop is in the allowed list.
|
|
94
|
+
#
|
|
95
|
+
# @param [String] cop The cop name.
|
|
96
|
+
# @return [Boolean]
|
|
97
|
+
def allowed_cop?(cop)
|
|
98
|
+
allowed_cops.include?(cop)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Get the list of allowed cops from configuration.
|
|
102
|
+
#
|
|
103
|
+
# @return [Array<String>]
|
|
104
|
+
def allowed_cops
|
|
105
|
+
@allowed_cops ||= Array(cop_config.fetch("AllowedCops", []))
|
|
106
|
+
end
|
|
57
107
|
end
|
|
58
108
|
end
|
|
59
109
|
end
|
|
@@ -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,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Vibe
|
|
6
|
+
# Enforces using `if/end` blocks instead of inline modifiers for `raise` statements.
|
|
7
|
+
#
|
|
8
|
+
# Inline `raise ... if/unless` statements can be harder to read because they
|
|
9
|
+
# place the condition after the action. This cop enforces converting them
|
|
10
|
+
# to `if/end` blocks for better readability.
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
# # bad - inline unless modifier
|
|
14
|
+
# raise ArgumentError, "Invalid column" unless COLUMNS.include?(column)
|
|
15
|
+
#
|
|
16
|
+
# # good - if block with negated condition
|
|
17
|
+
# if !COLUMNS.include?(column)
|
|
18
|
+
# raise ArgumentError, "Invalid column"
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# # bad - inline if modifier
|
|
22
|
+
# raise ArgumentError, "Invalid column" if column.nil?
|
|
23
|
+
#
|
|
24
|
+
# # good - if block
|
|
25
|
+
# if column.nil?
|
|
26
|
+
# raise ArgumentError, "Invalid column"
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
# # good - raise without condition
|
|
30
|
+
# raise ArgumentError, "Invalid column"
|
|
31
|
+
class RaiseUnlessBlock < Base
|
|
32
|
+
extend AutoCorrector
|
|
33
|
+
|
|
34
|
+
MSG = "Use `if/end` block instead of inline modifier for `raise`."
|
|
35
|
+
|
|
36
|
+
# Check if nodes for raise with if/unless modifier.
|
|
37
|
+
#
|
|
38
|
+
# @param [RuboCop::AST::Node] node The if node.
|
|
39
|
+
# @return [void]
|
|
40
|
+
def on_if(node)
|
|
41
|
+
return unless node.modifier_form?
|
|
42
|
+
return unless raise_call?(node.body)
|
|
43
|
+
|
|
44
|
+
add_offense(node) do |corrector|
|
|
45
|
+
autocorrect(corrector, node)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
# Check if the node is a raise method call.
|
|
52
|
+
#
|
|
53
|
+
# @param [RuboCop::AST::Node] node The node to check.
|
|
54
|
+
# @return [Boolean]
|
|
55
|
+
def raise_call?(node)
|
|
56
|
+
node.send_type? && node.method?(:raise)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Autocorrect the offense by converting to if block.
|
|
60
|
+
#
|
|
61
|
+
# @param [RuboCop::Cop::Corrector] corrector The corrector.
|
|
62
|
+
# @param [RuboCop::AST::Node] node The if node.
|
|
63
|
+
# @return [void]
|
|
64
|
+
def autocorrect(corrector, node)
|
|
65
|
+
replacement = build_replacement(node)
|
|
66
|
+
|
|
67
|
+
corrector.replace(node, replacement)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Build the replacement code for the raise statement.
|
|
71
|
+
#
|
|
72
|
+
# @param [RuboCop::AST::Node] node The if node.
|
|
73
|
+
# @return [String] The replacement code.
|
|
74
|
+
def build_replacement(node)
|
|
75
|
+
condition = build_condition(node)
|
|
76
|
+
raise_source = node.body.source
|
|
77
|
+
base_indent = " " * node.loc.column
|
|
78
|
+
inner_indent = "#{base_indent} "
|
|
79
|
+
|
|
80
|
+
[
|
|
81
|
+
"if #{condition}",
|
|
82
|
+
"#{inner_indent}#{raise_source}",
|
|
83
|
+
"#{base_indent}end"
|
|
84
|
+
].join("\n")
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Build the condition for the if block.
|
|
88
|
+
# For unless, negate the condition. For if, keep it as is.
|
|
89
|
+
#
|
|
90
|
+
# @param [RuboCop::AST::Node] node The if node.
|
|
91
|
+
# @return [String] The condition source.
|
|
92
|
+
def build_condition(node)
|
|
93
|
+
if node.unless?
|
|
94
|
+
"!#{node.condition.source}"
|
|
95
|
+
else
|
|
96
|
+
node.condition.source
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|