rubocop-vibe 0.2.0 → 0.4.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 +51 -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_instance_variable_assignment_alignment.rb +123 -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/let_order.rb +147 -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 +9 -0
- data/lib/rubocop/vibe/plugin.rb +4 -4
- data/lib/rubocop/vibe/version.rb +1 -1
- metadata +11 -2
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Vibe
|
|
6
|
+
# Enforces using explicit `if`/`else`/`end` blocks instead of ternary operators
|
|
7
|
+
# or trailing conditionals when they are the return value of a method.
|
|
8
|
+
#
|
|
9
|
+
# Ternary operators and trailing conditionals can be harder to read when used
|
|
10
|
+
# as method return values. This cop enforces converting them to explicit
|
|
11
|
+
# `if`/`else`/`end` blocks for better readability.
|
|
12
|
+
#
|
|
13
|
+
# @example
|
|
14
|
+
# # bad - ternary as return value
|
|
15
|
+
# def allow_origin
|
|
16
|
+
# origin = request.headers["Origin"]
|
|
17
|
+
# origin_allowed?(origin) ? origin : "*"
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# # good - explicit if/else
|
|
21
|
+
# def allow_origin
|
|
22
|
+
# origin = request.headers["Origin"]
|
|
23
|
+
# if origin_allowed?(origin)
|
|
24
|
+
# origin
|
|
25
|
+
# else
|
|
26
|
+
# "*"
|
|
27
|
+
# end
|
|
28
|
+
# end
|
|
29
|
+
#
|
|
30
|
+
# # bad - trailing conditional as return value
|
|
31
|
+
# def vary
|
|
32
|
+
# "Origin" if website.present?
|
|
33
|
+
# end
|
|
34
|
+
#
|
|
35
|
+
# # good - explicit if block
|
|
36
|
+
# def vary
|
|
37
|
+
# if website.present?
|
|
38
|
+
# "Origin"
|
|
39
|
+
# end
|
|
40
|
+
# end
|
|
41
|
+
#
|
|
42
|
+
# # good - ternary used in assignment (not as return value)
|
|
43
|
+
# def example
|
|
44
|
+
# result = condition ? "yes" : "no"
|
|
45
|
+
# process(result)
|
|
46
|
+
# end
|
|
47
|
+
class ExplicitReturnConditional < Base
|
|
48
|
+
extend AutoCorrector
|
|
49
|
+
|
|
50
|
+
MSG_TERNARY = "Use explicit `if`/`else`/`end` block instead of ternary operator for return value."
|
|
51
|
+
MSG_MODIFIER = "Use explicit `if`/`end` block instead of trailing conditional for return value."
|
|
52
|
+
|
|
53
|
+
# Check method definitions for conditional return values.
|
|
54
|
+
#
|
|
55
|
+
# @param [RuboCop::AST::Node] node The def node.
|
|
56
|
+
# @return [void]
|
|
57
|
+
def on_def(node)
|
|
58
|
+
if node.body
|
|
59
|
+
check_return_value(node.body)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
alias on_defs on_def
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
# Check if the return value is a conditional that should be explicit.
|
|
67
|
+
#
|
|
68
|
+
# @param [RuboCop::AST::Node] body The method body node.
|
|
69
|
+
# @return [void]
|
|
70
|
+
def check_return_value(body)
|
|
71
|
+
return_node = find_return_node(body)
|
|
72
|
+
|
|
73
|
+
return unless return_node.if_type?
|
|
74
|
+
|
|
75
|
+
if return_node.ternary?
|
|
76
|
+
register_ternary_offense(return_node)
|
|
77
|
+
elsif return_node.modifier_form?
|
|
78
|
+
register_modifier_offense(return_node)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Find the node that represents the return value.
|
|
83
|
+
#
|
|
84
|
+
# @param [RuboCop::AST::Node] body The method body node.
|
|
85
|
+
# @return [RuboCop::AST::Node, nil]
|
|
86
|
+
def find_return_node(body)
|
|
87
|
+
if body.begin_type?
|
|
88
|
+
body.children.last
|
|
89
|
+
else
|
|
90
|
+
body
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Register offense for ternary operator.
|
|
95
|
+
#
|
|
96
|
+
# @param [RuboCop::AST::Node] node The ternary node.
|
|
97
|
+
# @return [void]
|
|
98
|
+
def register_ternary_offense(node)
|
|
99
|
+
add_offense(node, message: MSG_TERNARY) do |corrector|
|
|
100
|
+
autocorrect_ternary(corrector, node)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Register offense for modifier conditional.
|
|
105
|
+
#
|
|
106
|
+
# @param [RuboCop::AST::Node] node The modifier if node.
|
|
107
|
+
# @return [void]
|
|
108
|
+
def register_modifier_offense(node)
|
|
109
|
+
add_offense(node, message: MSG_MODIFIER) do |corrector|
|
|
110
|
+
autocorrect_modifier(corrector, node)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Autocorrect ternary operator to if/else/end block.
|
|
115
|
+
#
|
|
116
|
+
# @param [RuboCop::Cop::Corrector] corrector The corrector.
|
|
117
|
+
# @param [RuboCop::AST::Node] node The ternary node.
|
|
118
|
+
# @return [void]
|
|
119
|
+
def autocorrect_ternary(corrector, node)
|
|
120
|
+
corrector.replace(node, build_if_else_block(node))
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Autocorrect modifier conditional to if/end block.
|
|
124
|
+
#
|
|
125
|
+
# @param [RuboCop::Cop::Corrector] corrector The corrector.
|
|
126
|
+
# @param [RuboCop::AST::Node] node The modifier if node.
|
|
127
|
+
# @return [void]
|
|
128
|
+
def autocorrect_modifier(corrector, node)
|
|
129
|
+
corrector.replace(node, build_if_block(node))
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Build if/else/end block replacement for ternary.
|
|
133
|
+
#
|
|
134
|
+
# @param [RuboCop::AST::Node] node The ternary node.
|
|
135
|
+
# @return [String]
|
|
136
|
+
def build_if_else_block(node)
|
|
137
|
+
base_indent = " " * node.loc.column
|
|
138
|
+
inner_indent = "#{base_indent} "
|
|
139
|
+
|
|
140
|
+
[
|
|
141
|
+
"if #{node.condition.source}",
|
|
142
|
+
"#{inner_indent}#{node.if_branch.source}",
|
|
143
|
+
"#{base_indent}else",
|
|
144
|
+
"#{inner_indent}#{node.else_branch.source}",
|
|
145
|
+
"#{base_indent}end"
|
|
146
|
+
].join("\n")
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Build if/end block replacement for modifier conditional.
|
|
150
|
+
#
|
|
151
|
+
# @param [RuboCop::AST::Node] node The modifier if node.
|
|
152
|
+
# @return [String]
|
|
153
|
+
def build_if_block(node)
|
|
154
|
+
condition = build_condition(node)
|
|
155
|
+
base_indent = " " * node.loc.column
|
|
156
|
+
inner_indent = "#{base_indent} "
|
|
157
|
+
|
|
158
|
+
[
|
|
159
|
+
"if #{condition}",
|
|
160
|
+
"#{inner_indent}#{node.if_branch.source}",
|
|
161
|
+
"#{base_indent}end"
|
|
162
|
+
].join("\n")
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Build the condition for the if block.
|
|
166
|
+
# For unless, negate the condition. For if, keep it as is.
|
|
167
|
+
#
|
|
168
|
+
# @param [RuboCop::AST::Node] node The if node.
|
|
169
|
+
# @return [String] The condition source.
|
|
170
|
+
def build_condition(node)
|
|
171
|
+
if node.unless?
|
|
172
|
+
negate_condition(node.condition)
|
|
173
|
+
else
|
|
174
|
+
node.condition.source
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Negate a condition, handling simple cases cleanly.
|
|
179
|
+
#
|
|
180
|
+
# @param [RuboCop::AST::Node] condition The condition node.
|
|
181
|
+
# @return [String] The negated condition.
|
|
182
|
+
def negate_condition(condition)
|
|
183
|
+
if condition.send_type? && condition.method?(:!)
|
|
184
|
+
condition.receiver.source
|
|
185
|
+
else
|
|
186
|
+
"!#{condition.source}"
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
@@ -45,6 +45,7 @@ module RuboCop
|
|
|
45
45
|
return unless is_expected_call?(node)
|
|
46
46
|
|
|
47
47
|
example_block = find_example_block(node)
|
|
48
|
+
|
|
48
49
|
return unless example_block
|
|
49
50
|
return unless example_block_with_description?(example_block)
|
|
50
51
|
return if complex_expectation?(example_block)
|
|
@@ -65,6 +66,7 @@ module RuboCop
|
|
|
65
66
|
def find_example_block(node)
|
|
66
67
|
node.each_ancestor(:block).find do |ancestor|
|
|
67
68
|
send_node = ancestor.send_node
|
|
69
|
+
|
|
68
70
|
send_node.method?(:it) || send_node.method?(:specify)
|
|
69
71
|
end
|
|
70
72
|
end
|
|
@@ -100,6 +102,7 @@ module RuboCop
|
|
|
100
102
|
# @return [void]
|
|
101
103
|
def autocorrect(corrector, node)
|
|
102
104
|
expectation_source = node.body.source
|
|
105
|
+
|
|
103
106
|
corrector.replace(node, "it { #{expectation_source} }")
|
|
104
107
|
end
|
|
105
108
|
end
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Vibe
|
|
6
|
+
# Enforces alphabetical ordering of consecutive `let` declarations.
|
|
7
|
+
#
|
|
8
|
+
# Consecutive `let` declarations (with no blank lines between) should be
|
|
9
|
+
# alphabetically ordered by their symbol name for better readability and
|
|
10
|
+
# easier scanning. Groups are broken by blank lines or non-let statements.
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
# # bad
|
|
14
|
+
# let(:subcategory) { create(:category, :subcategory) }
|
|
15
|
+
# let(:budget) { subcategory.budget }
|
|
16
|
+
# let(:category) { subcategory.parent }
|
|
17
|
+
#
|
|
18
|
+
# # good
|
|
19
|
+
# let(:budget) { subcategory.budget }
|
|
20
|
+
# let(:category) { subcategory.parent }
|
|
21
|
+
# let(:subcategory) { create(:category, :subcategory) }
|
|
22
|
+
#
|
|
23
|
+
# # good - blank line breaks the group
|
|
24
|
+
# let(:zebra) { create(:zebra) }
|
|
25
|
+
#
|
|
26
|
+
# let(:apple) { create(:apple) }
|
|
27
|
+
class LetOrder < Base
|
|
28
|
+
extend AutoCorrector
|
|
29
|
+
include SpecFileHelper
|
|
30
|
+
include AlignmentHelpers
|
|
31
|
+
|
|
32
|
+
MSG = "Order consecutive `let` declarations alphabetically."
|
|
33
|
+
|
|
34
|
+
# @!method let_declaration?(node)
|
|
35
|
+
# Check if node is a let/let! declaration.
|
|
36
|
+
def_node_matcher :let_declaration?, <<~PATTERN
|
|
37
|
+
(block (send nil? {:let :let!} (sym _)) ...)
|
|
38
|
+
PATTERN
|
|
39
|
+
|
|
40
|
+
# Check describe/context blocks for let ordering.
|
|
41
|
+
#
|
|
42
|
+
# @param [RuboCop::AST::Node] node The block node.
|
|
43
|
+
# @return [void]
|
|
44
|
+
def on_block(node)
|
|
45
|
+
return unless spec_file?
|
|
46
|
+
return unless describe_or_context?(node)
|
|
47
|
+
return unless node.body
|
|
48
|
+
|
|
49
|
+
check_lets_in_body(node.body)
|
|
50
|
+
end
|
|
51
|
+
alias on_numblock on_block
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
# Check if block is a describe or context block.
|
|
56
|
+
#
|
|
57
|
+
# @param [RuboCop::AST::Node] node The block node.
|
|
58
|
+
# @return [Boolean]
|
|
59
|
+
def describe_or_context?(node)
|
|
60
|
+
node.send_node && %i(describe context).include?(node.method_name)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Check let declarations in a body node.
|
|
64
|
+
#
|
|
65
|
+
# @param [RuboCop::AST::Node] body The body node.
|
|
66
|
+
# @return [void]
|
|
67
|
+
def check_lets_in_body(body)
|
|
68
|
+
statements = extract_statements(body)
|
|
69
|
+
|
|
70
|
+
return if statements.size < 2
|
|
71
|
+
|
|
72
|
+
groups = group_consecutive_statements(statements) { |s| let_declaration?(s) }
|
|
73
|
+
|
|
74
|
+
groups.each { |group| check_group_order(group) }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Check ordering for a group of let declarations.
|
|
78
|
+
#
|
|
79
|
+
# @param [Array<RuboCop::AST::Node>] group The let group.
|
|
80
|
+
# @return [void]
|
|
81
|
+
def check_group_order(group)
|
|
82
|
+
return if alphabetically_ordered?(group)
|
|
83
|
+
|
|
84
|
+
violations = find_ordering_violations(group)
|
|
85
|
+
|
|
86
|
+
violations.each do |let|
|
|
87
|
+
add_offense(let.send_node) do |corrector|
|
|
88
|
+
autocorrect(corrector, group)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Check if let declarations are alphabetically ordered.
|
|
94
|
+
#
|
|
95
|
+
# @param [Array<RuboCop::AST::Node>] group The let group.
|
|
96
|
+
# @return [Boolean]
|
|
97
|
+
def alphabetically_ordered?(group)
|
|
98
|
+
names = group.map { |let| extract_let_name(let) }
|
|
99
|
+
|
|
100
|
+
names == names.sort
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Extract the symbol name from a let declaration.
|
|
104
|
+
#
|
|
105
|
+
# @param [RuboCop::AST::Node] let The let block node.
|
|
106
|
+
# @return [String]
|
|
107
|
+
def extract_let_name(let)
|
|
108
|
+
let.send_node.first_argument.value.to_s
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Find let declarations that violate ordering.
|
|
112
|
+
#
|
|
113
|
+
# @param [Array<RuboCop::AST::Node>] group The let group.
|
|
114
|
+
# @return [Array<RuboCop::AST::Node>] Lets that violate ordering.
|
|
115
|
+
def find_ordering_violations(group)
|
|
116
|
+
violations = []
|
|
117
|
+
|
|
118
|
+
group.each_cons(2) do |current, following|
|
|
119
|
+
current_name = extract_let_name(current)
|
|
120
|
+
following_name = extract_let_name(following)
|
|
121
|
+
|
|
122
|
+
violations << following if current_name > following_name
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
violations.uniq
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Auto-correct by reordering let declarations.
|
|
129
|
+
#
|
|
130
|
+
# @param [RuboCop::AST::Corrector] corrector The corrector.
|
|
131
|
+
# @param [Array<RuboCop::AST::Node>] group The let group.
|
|
132
|
+
# @return [void]
|
|
133
|
+
def autocorrect(corrector, group)
|
|
134
|
+
sorted = group.sort_by { |let| extract_let_name(let) }
|
|
135
|
+
|
|
136
|
+
group.each_with_index do |let, index|
|
|
137
|
+
sorted_let = sorted[index]
|
|
138
|
+
|
|
139
|
+
next if let == sorted_let
|
|
140
|
+
|
|
141
|
+
corrector.replace(let, sorted_let.source)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
@@ -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
|