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.
Files changed (28) hide show
  1. checksums.yaml +4 -4
  2. data/config/default.yml +51 -4
  3. data/lib/rubocop/cop/vibe/blank_line_after_assignment.rb +217 -0
  4. data/lib/rubocop/cop/vibe/blank_line_before_expectation.rb +6 -1
  5. data/lib/rubocop/cop/vibe/class_organization.rb +20 -1
  6. data/lib/rubocop/cop/vibe/consecutive_assignment_alignment.rb +5 -102
  7. data/lib/rubocop/cop/vibe/consecutive_constant_alignment.rb +5 -113
  8. data/lib/rubocop/cop/vibe/consecutive_indexed_assignment_alignment.rb +145 -0
  9. data/lib/rubocop/cop/vibe/consecutive_instance_variable_assignment_alignment.rb +123 -0
  10. data/lib/rubocop/cop/vibe/consecutive_let_alignment.rb +5 -94
  11. data/lib/rubocop/cop/vibe/describe_block_order.rb +6 -2
  12. data/lib/rubocop/cop/vibe/explicit_return_conditional.rb +192 -0
  13. data/lib/rubocop/cop/vibe/is_expected_one_liner.rb +3 -0
  14. data/lib/rubocop/cop/vibe/let_order.rb +147 -0
  15. data/lib/rubocop/cop/vibe/mixin/alignment_helpers.rb +92 -0
  16. data/lib/rubocop/cop/vibe/multiline_hash_argument_style.rb +171 -0
  17. data/lib/rubocop/cop/vibe/no_compound_conditions.rb +138 -0
  18. data/lib/rubocop/cop/vibe/no_rubocop_disable.rb +3 -0
  19. data/lib/rubocop/cop/vibe/no_skipped_tests.rb +1 -0
  20. data/lib/rubocop/cop/vibe/no_unless_guard_clause.rb +4 -0
  21. data/lib/rubocop/cop/vibe/prefer_one_liner_expectation.rb +4 -0
  22. data/lib/rubocop/cop/vibe/raise_unless_block.rb +1 -0
  23. data/lib/rubocop/cop/vibe/rspec_before_block_style.rb +114 -0
  24. data/lib/rubocop/cop/vibe/rspec_stub_chain_style.rb +4 -0
  25. data/lib/rubocop/cop/vibe_cops.rb +9 -0
  26. data/lib/rubocop/vibe/plugin.rb +4 -4
  27. data/lib/rubocop/vibe/version.rb +1 -1
  28. 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