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.
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Vibe
6
+ # Enforces alignment of consecutive constant assignments at the `=` operator.
7
+ #
8
+ # Consecutive constant assignments (with no blank lines between) should align their
9
+ # `=` operators for better readability. Groups are broken by blank lines or non-constant
10
+ # statements.
11
+ #
12
+ # @example
13
+ # # bad
14
+ # MINIMUM_NAME_LENGTH = 3
15
+ # MAXIMUM_NAME_LENGTH = 12
16
+ # ACTIVE_DURATION = 15.minutes
17
+ #
18
+ # # good
19
+ # MINIMUM_NAME_LENGTH = 3
20
+ # MAXIMUM_NAME_LENGTH = 12
21
+ # ACTIVE_DURATION = 15.minutes
22
+ #
23
+ # # good - blank line breaks the group
24
+ # THROTTLE_LIMIT = 10
25
+ # THROTTLE_PERIOD = 5
26
+ #
27
+ # DEFAULT_VALUE = 0 # Separate group, not aligned
28
+ class ConsecutiveConstantAlignment < Base
29
+ extend AutoCorrector
30
+ include AlignmentHelpers
31
+
32
+ MSG = "Align consecutive constant assignments at the `=` operator."
33
+
34
+ # Check class nodes for constant alignment.
35
+ #
36
+ # @param [RuboCop::AST::Node] node The class node.
37
+ # @return [void]
38
+ def on_class(node)
39
+ if node.body
40
+ check_constants_in_body(node.body)
41
+ end
42
+ end
43
+
44
+ # Check module nodes for constant alignment.
45
+ #
46
+ # @param [RuboCop::AST::Node] node The module node.
47
+ # @return [void]
48
+ def on_module(node)
49
+ if node.body
50
+ check_constants_in_body(node.body)
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ # Check constants in a body node.
57
+ #
58
+ # @param [RuboCop::AST::Node] body The body node.
59
+ # @return [void]
60
+ def check_constants_in_body(body)
61
+ statements = extract_statements(body)
62
+
63
+ return if statements.size < 2
64
+
65
+ groups = group_consecutive_statements(statements) { |s| s.casgn_type? && s.single_line? }
66
+
67
+ groups.each { |group| check_group_alignment(group) }
68
+ end
69
+
70
+ # Check alignment for a group of constant assignments.
71
+ #
72
+ # @param [Array<RuboCop::AST::Node>] group The constant group.
73
+ # @return [void]
74
+ def check_group_alignment(group)
75
+ columns = group.map { |const| const.loc.operator.column }
76
+ target_column = columns.max
77
+
78
+ group.each do |const|
79
+ current_column = const.loc.operator.column
80
+
81
+ next if current_column == target_column
82
+
83
+ add_offense(const.loc.name) do |corrector|
84
+ autocorrect_alignment(corrector, const, target_column)
85
+ end
86
+ end
87
+ end
88
+
89
+ # Auto-correct the alignment of a constant assignment.
90
+ #
91
+ # @param [RuboCop::AST::Corrector] corrector The corrector.
92
+ # @param [RuboCop::AST::Node] const The constant assignment node.
93
+ # @param [Integer] target_column The target column for alignment.
94
+ # @return [void]
95
+ def autocorrect_alignment(corrector, const, target_column)
96
+ name_end = const.loc.name.end_pos
97
+ operator_start = const.loc.operator.begin_pos
98
+ total_spaces = calculate_total_spaces(const, target_column)
99
+
100
+ corrector.replace(
101
+ range_between(name_end, operator_start),
102
+ " " * total_spaces
103
+ )
104
+ end
105
+
106
+ # Calculate total spaces needed for alignment.
107
+ #
108
+ # @param [RuboCop::AST::Node] const The constant assignment node.
109
+ # @param [Integer] target_column The target column for alignment.
110
+ # @return [Integer] The number of spaces (minimum 1).
111
+ def calculate_total_spaces(const, target_column)
112
+ current_column = const.loc.operator.column
113
+ current_spaces = const.loc.operator.begin_pos - const.loc.name.end_pos
114
+ spaces_needed = target_column - current_column
115
+
116
+ [1, current_spaces + spaces_needed].max
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Vibe
6
+ # Enforces alignment of consecutive indexed assignments at the `=` operator.
7
+ #
8
+ # Consecutive indexed assignments (with no blank lines between) should align
9
+ # their `=` operators for better readability. Groups are broken by blank lines.
10
+ #
11
+ # @example
12
+ # # bad
13
+ # response.headers["Cache-Control"] = "public, max-age=3600"
14
+ # response.headers["Content-Type"] = "application/javascript"
15
+ #
16
+ # # good
17
+ # response.headers["Cache-Control"] = "public, max-age=3600"
18
+ # response.headers["Content-Type"] = "application/javascript"
19
+ #
20
+ # # good - blank line breaks the group
21
+ # response.headers["Cache-Control"] = "public, max-age=3600"
22
+ #
23
+ # hash["key"] = "value" # Separate group, not aligned
24
+ class ConsecutiveIndexedAssignmentAlignment < Base
25
+ extend AutoCorrector
26
+ include AlignmentHelpers
27
+
28
+ MSG = "Align consecutive indexed assignments at the = operator."
29
+
30
+ # Check block nodes for indexed assignment alignment.
31
+ #
32
+ # @param [RuboCop::AST::Node] node The block node.
33
+ # @return [void]
34
+ def on_block(node)
35
+ if node.body
36
+ check_indexed_assignments_in_body(node.body)
37
+ end
38
+ end
39
+ alias on_numblock on_block
40
+
41
+ # Check method definitions for indexed assignment alignment.
42
+ #
43
+ # @param [RuboCop::AST::Node] node The def node.
44
+ # @return [void]
45
+ def on_def(node)
46
+ if node.body
47
+ check_indexed_assignments_in_body(node.body)
48
+ end
49
+ end
50
+ alias on_defs on_def
51
+
52
+ private
53
+
54
+ # Check indexed assignments in a body node.
55
+ #
56
+ # @param [RuboCop::AST::Node] body The body node.
57
+ # @return [void]
58
+ def check_indexed_assignments_in_body(body)
59
+ statements = extract_statements(body)
60
+
61
+ return if statements.size < 2
62
+
63
+ groups = group_consecutive_statements(statements) { |s| indexed_assignment?(s) }
64
+
65
+ groups.each { |group| check_group_alignment(group) }
66
+ end
67
+
68
+ # Check if a node is an indexed assignment.
69
+ #
70
+ # @param [RuboCop::AST::Node] node The node to check.
71
+ # @return [Boolean]
72
+ def indexed_assignment?(node)
73
+ node.send_type? && node.method?(:[]=)
74
+ end
75
+
76
+ # Check alignment for a group of indexed assignments.
77
+ #
78
+ # @param [Array<RuboCop::AST::Node>] group The assignment group.
79
+ # @return [void]
80
+ def check_group_alignment(group)
81
+ columns = group.map { |asgn| asgn.loc.operator.column }
82
+ target_column = columns.max
83
+
84
+ group.each do |asgn|
85
+ current_column = asgn.loc.operator.column
86
+
87
+ next if current_column == target_column
88
+
89
+ add_offense(offense_location(asgn)) do |corrector|
90
+ autocorrect_alignment(corrector, asgn, target_column)
91
+ end
92
+ end
93
+ end
94
+
95
+ # Get the location to highlight for the offense.
96
+ #
97
+ # @param [RuboCop::AST::Node] asgn The indexed assignment node.
98
+ # @return [Parser::Source::Range]
99
+ def offense_location(asgn)
100
+ asgn.loc.selector
101
+ end
102
+
103
+ # Auto-correct the alignment of an indexed assignment.
104
+ #
105
+ # @param [RuboCop::AST::Corrector] corrector The corrector.
106
+ # @param [RuboCop::AST::Node] asgn The indexed assignment node.
107
+ # @param [Integer] target_column The target column for alignment.
108
+ # @return [void]
109
+ def autocorrect_alignment(corrector, asgn, target_column)
110
+ bracket_end = closing_bracket_end_pos(asgn)
111
+ operator_start = asgn.loc.operator.begin_pos
112
+ total_spaces = calculate_total_spaces(asgn, target_column, bracket_end, operator_start)
113
+
114
+ corrector.replace(
115
+ range_between(bracket_end, operator_start),
116
+ " " * total_spaces
117
+ )
118
+ end
119
+
120
+ # Get the position after the closing bracket.
121
+ #
122
+ # @param [RuboCop::AST::Node] asgn The indexed assignment node.
123
+ # @return [Integer]
124
+ def closing_bracket_end_pos(asgn)
125
+ asgn.first_argument.source_range.end_pos + 1
126
+ end
127
+
128
+ # Calculate total spaces needed for alignment.
129
+ #
130
+ # @param [RuboCop::AST::Node] asgn The indexed assignment node.
131
+ # @param [Integer] target_column The target column for alignment.
132
+ # @param [Integer] bracket_end Position after the closing bracket.
133
+ # @param [Integer] operator_start Position of the operator.
134
+ # @return [Integer] The number of spaces (minimum 1).
135
+ def calculate_total_spaces(asgn, target_column, bracket_end, operator_start)
136
+ current_column = asgn.loc.operator.column
137
+ current_spaces = operator_start - bracket_end
138
+ spaces_needed = target_column - current_column
139
+
140
+ [1, current_spaces + spaces_needed].max
141
+ end
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Vibe
6
+ # Enforces alignment of consecutive `let` declarations at the `{` brace.
7
+ #
8
+ # Consecutive `let` declarations (with no blank lines between) should align their
9
+ # `{` braces for better readability. Groups are broken by blank lines or non-let
10
+ # statements.
11
+ #
12
+ # @example
13
+ # # bad
14
+ # let(:character) { instance_double(Character) }
15
+ # let(:damage) { 1 }
16
+ # let(:instance) { described_class.new }
17
+ #
18
+ # # good
19
+ # let(:character) { instance_double(Character) }
20
+ # let(:damage) { 1 }
21
+ # let(:instance) { described_class.new }
22
+ #
23
+ # # good - blank line breaks the group
24
+ # let(:user) { create(:user) }
25
+ # let(:character) { create(:character) }
26
+ #
27
+ # let(:service) { Users::Activate.new } # Separate group, not aligned
28
+ class ConsecutiveLetAlignment < Base
29
+ extend AutoCorrector
30
+ include SpecFileHelper
31
+ include AlignmentHelpers
32
+
33
+ MSG = "Align consecutive `let` declarations at the `{` brace."
34
+
35
+ # @!method let_declaration?(node)
36
+ # Check if node is a let/let! declaration.
37
+ def_node_matcher :let_declaration?, <<~PATTERN
38
+ (block (send nil? {:let :let!} (sym _)) ...)
39
+ PATTERN
40
+
41
+ # Check describe/context blocks for let alignment.
42
+ #
43
+ # @param [RuboCop::AST::Node] node The block node.
44
+ # @return [void]
45
+ def on_block(node)
46
+ return unless spec_file?
47
+ return unless describe_or_context?(node)
48
+ return unless node.body
49
+
50
+ check_lets_in_body(node.body)
51
+ end
52
+ alias on_numblock on_block
53
+
54
+ private
55
+
56
+ # Check if block is a describe or context block.
57
+ #
58
+ # @param [RuboCop::AST::Node] node The block node.
59
+ # @return [Boolean]
60
+ def describe_or_context?(node)
61
+ node.send_node && %i(describe context).include?(node.method_name)
62
+ end
63
+
64
+ # Check let declarations in a body node.
65
+ #
66
+ # @param [RuboCop::AST::Node] body The body node.
67
+ # @return [void]
68
+ def check_lets_in_body(body)
69
+ statements = extract_statements(body)
70
+
71
+ return if statements.size < 2
72
+
73
+ groups = group_consecutive_statements(statements) { |s| let_declaration?(s) }
74
+
75
+ groups.each { |group| check_group_alignment(group) }
76
+ end
77
+
78
+ # Check alignment for a group of let declarations.
79
+ #
80
+ # @param [Array<RuboCop::AST::Node>] group The let group.
81
+ # @return [void]
82
+ def check_group_alignment(group)
83
+ columns = group.map { |let| let.loc.begin.column }
84
+ target_column = columns.max
85
+
86
+ group.each do |let|
87
+ current_column = let.loc.begin.column
88
+
89
+ next if current_column == target_column
90
+
91
+ add_offense(let.send_node) do |corrector|
92
+ autocorrect_alignment(corrector, let, target_column)
93
+ end
94
+ end
95
+ end
96
+
97
+ # Auto-correct the alignment of a let declaration.
98
+ #
99
+ # @param [RuboCop::AST::Corrector] corrector The corrector.
100
+ # @param [RuboCop::AST::Node] let The let block node.
101
+ # @param [Integer] target_column The target column for alignment.
102
+ # @return [void]
103
+ def autocorrect_alignment(corrector, let, target_column)
104
+ send_node = let.send_node
105
+ send_end = send_node.source_range.end_pos
106
+ brace_start = let.loc.begin.begin_pos
107
+ total_spaces = calculate_total_spaces(let, target_column)
108
+
109
+ corrector.replace(
110
+ range_between(send_end, brace_start),
111
+ " " * total_spaces
112
+ )
113
+ end
114
+
115
+ # Calculate total spaces needed for alignment.
116
+ #
117
+ # @param [RuboCop::AST::Node] let The let block node.
118
+ # @param [Integer] target_column The target column for alignment.
119
+ # @return [Integer] The number of spaces (minimum 1).
120
+ def calculate_total_spaces(let, target_column)
121
+ current_column = let.loc.begin.column
122
+ current_spaces = let.loc.begin.begin_pos - let.send_node.source_range.end_pos
123
+ spaces_needed = target_column - current_column
124
+
125
+ [1, current_spaces + spaces_needed].max
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
@@ -59,7 +59,7 @@ module RuboCop
59
59
  # Default priority for descriptions that can't be categorized (e.g., constants, variables)
60
60
  DEFAULT_PRIORITY = 999
61
61
 
62
- MODEL_ORDER = %w(class associations validations).freeze
62
+ MODEL_ORDER = %w(class associations validations).freeze
63
63
  CONTROLLER_ACTIONS = %w(index show new create edit update destroy).freeze
64
64
  SPECIAL_SECTIONS = {
65
65
  "class" => 0,
@@ -83,9 +83,11 @@ module RuboCop
83
83
  return unless top_level_describe?(node)
84
84
 
85
85
  describe_blocks = extract_describe_blocks(node)
86
+
86
87
  return if describe_blocks.size < 2
87
88
 
88
89
  violations = find_ordering_violations(describe_blocks)
90
+
89
91
  violations.each do |block_info|
90
92
  add_offense(block_info[:node]) do |corrector|
91
93
  autocorrect(corrector, describe_blocks)
@@ -153,15 +155,15 @@ module RuboCop
153
155
  # @return [nil] When description is not a string/symbol literal.
154
156
  def extract_description(node)
155
157
  first_arg = node.send_node.first_argument
158
+
156
159
  return unless first_arg
157
160
 
158
161
  if first_arg.str_type?
159
162
  first_arg.value
160
163
  elsif first_arg.sym_type?
161
164
  first_arg.value.to_s
162
- # Intentionally returns nil for constants/variables.
163
- # These will get DEFAULT_PRIORITY.
164
165
  end
166
+ # Returns nil for constants/variables - they get DEFAULT_PRIORITY.
165
167
  end
166
168
 
167
169
  # Categorize description and assign priority.
@@ -202,6 +204,7 @@ module RuboCop
202
204
  def controller_action_priority(description)
203
205
  # Strip the # prefix for controller actions.
204
206
  action_name = description.start_with?("#") ? description[1..] : description
207
+
205
208
  if controller_action?(action_name)
206
209
  30 + CONTROLLER_ACTIONS.index(action_name)
207
210
  end
@@ -251,6 +254,7 @@ module RuboCop
251
254
 
252
255
  blocks.each_with_index do |block, index|
253
256
  sorted_block = sorted_blocks[index]
257
+
254
258
  next if block == sorted_block
255
259
 
256
260
  corrector.replace(block[:node], sorted_block[:node].source)
@@ -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