rubocop-vibe 0.1.0 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 66c4596677b86001b40036ed73d0ce8300b9010cc3ea16af13436cc7a1ab3cfd
4
- data.tar.gz: b27f79a610368b858a565c70c90fcf7db80e0b258e7efc3ce17942ccfdef79cc
3
+ metadata.gz: e91f63c7c86e77f854c0dd424a1d40d1fd5ad8f0407836dc74467d59e786e6ab
4
+ data.tar.gz: d4cdc851197a8a1fcbbdebe14551c369029469f6c2e6c4a09b50060fb40b39f8
5
5
  SHA512:
6
- metadata.gz: 57f43f61aba7ac210141f1d3aafff845f9b9a356ab88a42174de2344035825377bfc3ed1603d37e4b9662208f013a027be6be28cf2da40a514acb44f517469a6
7
- data.tar.gz: fbe66437855c42030400323044fa53d3526e017fea1db8962d342aa9df7484072746a31c86791d7448c7f100b465ce72430318117d11967dfbb19fde09ee3d46
6
+ metadata.gz: eaebb568a44bd514577f6e3dc63a3b4a05d6c78762b5520462a0ba55119549bece813953d35cd2956fb03830a703d7a90e23d6b9c25204a3f41ee40281d87852
7
+ data.tar.gz: 0d66492bf54277b78dfa228bfdd7ef3cb2ba46f715ef32dc5304371d6af7ab9eff2e7c1a1ff3ca0fcd1ca4abd82ae8d01e41be9154fdb66a15b1546c504fb2cc
data/config/default.yml CHANGED
@@ -74,6 +74,18 @@ Vibe/ConsecutiveAssignmentAlignment:
74
74
  SafeAutoCorrect: true
75
75
  VersionAdded: "0.1.0"
76
76
 
77
+ Vibe/ConsecutiveConstantAlignment:
78
+ Description: "Enforces alignment of consecutive constant assignments at the = operator."
79
+ Enabled: true
80
+ SafeAutoCorrect: true
81
+ VersionAdded: "0.2.0"
82
+
83
+ Vibe/ConsecutiveLetAlignment:
84
+ Description: "Enforces alignment of consecutive let declarations at the { brace."
85
+ Enabled: true
86
+ SafeAutoCorrect: true
87
+ VersionAdded: "0.2.0"
88
+
77
89
  Vibe/DescribeBlockOrder:
78
90
  Description: "Enforces consistent ordering of describe blocks in RSpec files."
79
91
  Enabled: true
@@ -101,6 +113,7 @@ Vibe/NoAssignsAttributeTesting:
101
113
  Vibe/NoRubocopDisable:
102
114
  Description: "Enforces that rubocop:disable comments are not used inline."
103
115
  Enabled: true
116
+ AllowedCops: []
104
117
  VersionAdded: "0.1.0"
105
118
 
106
119
  Vibe/NoSkippedTests:
@@ -120,6 +133,18 @@ Vibe/PreferOneLinerExpectation:
120
133
  SafeAutoCorrect: true
121
134
  VersionAdded: "0.1.0"
122
135
 
136
+ Vibe/RaiseUnlessBlock:
137
+ Description: "Enforces using if/end blocks instead of inline modifiers for raise statements."
138
+ Enabled: true
139
+ SafeAutoCorrect: true
140
+ VersionAdded: "0.2.0"
141
+
142
+ Vibe/RspecStubChainStyle:
143
+ Description: "Enforces that RSpec stub chains are split across lines when too long."
144
+ Enabled: true
145
+ SafeAutoCorrect: true
146
+ VersionAdded: "0.2.0"
147
+
123
148
  Vibe/ServiceCallMethod:
124
149
  Description: "Service objects should define `self.call` and `call` methods."
125
150
  Enabled: true
@@ -0,0 +1,229 @@
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
+
31
+ MSG = "Align consecutive constant assignments at the `=` operator."
32
+
33
+ # Check class nodes for constant alignment.
34
+ #
35
+ # @param [RuboCop::AST::Node] node The class node.
36
+ # @return [void]
37
+ def on_class(node)
38
+ if node.body
39
+ check_constants_in_body(node.body)
40
+ end
41
+ end
42
+
43
+ # Check module nodes for constant alignment.
44
+ #
45
+ # @param [RuboCop::AST::Node] node The module node.
46
+ # @return [void]
47
+ def on_module(node)
48
+ if node.body
49
+ check_constants_in_body(node.body)
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ # Check constants in a body node.
56
+ #
57
+ # @param [RuboCop::AST::Node] body The body node.
58
+ # @return [void]
59
+ def check_constants_in_body(body)
60
+ statements = extract_statements(body)
61
+ return if statements.size < 2
62
+
63
+ groups = group_consecutive_constants(statements)
64
+ groups.each { |group| check_group_alignment(group) }
65
+ end
66
+
67
+ # Extract statements from a body node.
68
+ #
69
+ # @param [RuboCop::AST::Node] body The body node.
70
+ # @return [Array<RuboCop::AST::Node>]
71
+ def extract_statements(body)
72
+ if body.begin_type?
73
+ body.children
74
+ else
75
+ [body]
76
+ end
77
+ end
78
+
79
+ # Group consecutive constant assignments together.
80
+ #
81
+ # @param [Array<RuboCop::AST::Node>] statements The statements.
82
+ # @return [Array<Array<RuboCop::AST::Node>>] Groups of consecutive constants.
83
+ def group_consecutive_constants(statements)
84
+ groups = []
85
+ current_group = []
86
+ previous_line = nil
87
+
88
+ statements.each do |statement|
89
+ current_group, previous_line = process_statement(statement, current_group, previous_line, groups)
90
+ end
91
+
92
+ finalize_groups(groups, current_group)
93
+ end
94
+
95
+ # Process a single statement for grouping.
96
+ #
97
+ # @param [RuboCop::AST::Node] statement The statement.
98
+ # @param [Array<RuboCop::AST::Node>] current_group The current group.
99
+ # @param [Integer, nil] previous_line The previous line number.
100
+ # @param [Array<Array<RuboCop::AST::Node>>] groups The groups.
101
+ # @return [Array] The updated current_group and previous_line.
102
+ def process_statement(statement, current_group, previous_line, groups)
103
+ if constant_assignment?(statement)
104
+ current_group = handle_constant(statement, current_group, previous_line, groups)
105
+ else
106
+ save_group_if_valid(groups, current_group)
107
+ current_group = []
108
+ end
109
+ [current_group, statement.loc.last_line]
110
+ end
111
+
112
+ # Check if node is a single-line constant assignment.
113
+ #
114
+ # Only single-line constants are considered for alignment to avoid
115
+ # conflicts with multi-line hash/array constants and Layout/ExtraSpacing.
116
+ #
117
+ # @param [RuboCop::AST::Node] node The node.
118
+ # @return [Boolean]
119
+ def constant_assignment?(node)
120
+ node.casgn_type? && single_line?(node)
121
+ end
122
+
123
+ # Check if node is on a single line.
124
+ #
125
+ # @param [RuboCop::AST::Node] node The node.
126
+ # @return [Boolean]
127
+ def single_line?(node)
128
+ node.single_line?
129
+ end
130
+
131
+ # Handle a constant assignment.
132
+ #
133
+ # @param [RuboCop::AST::Node] statement The constant assignment.
134
+ # @param [Array<RuboCop::AST::Node>] current_group The current group.
135
+ # @param [Integer, nil] previous_line The previous line number.
136
+ # @param [Array<Array<RuboCop::AST::Node>>] groups The groups.
137
+ # @return [Array<RuboCop::AST::Node>] The updated current group.
138
+ def handle_constant(statement, current_group, previous_line, groups)
139
+ if previous_line && statement.loc.line - previous_line > 1
140
+ save_group_if_valid(groups, current_group)
141
+ current_group = []
142
+ end
143
+ current_group << statement
144
+ current_group
145
+ end
146
+
147
+ # Save group if it has multiple constant assignments.
148
+ #
149
+ # @param [Array<Array<RuboCop::AST::Node>>] groups The groups.
150
+ # @param [Array<RuboCop::AST::Node>] group The group to potentially save.
151
+ # @return [void]
152
+ def save_group_if_valid(groups, group)
153
+ groups << group if group.size > 1
154
+ end
155
+
156
+ # Finalize groups by adding any remaining valid group.
157
+ #
158
+ # @param [Array<Array<RuboCop::AST::Node>>] groups The groups.
159
+ # @param [Array<RuboCop::AST::Node>] current_group The current group.
160
+ # @return [Array<Array<RuboCop::AST::Node>>] The finalized groups.
161
+ def finalize_groups(groups, current_group)
162
+ save_group_if_valid(groups, current_group)
163
+ groups
164
+ end
165
+
166
+ # Check alignment for a group of constant assignments.
167
+ #
168
+ # @param [Array<RuboCop::AST::Node>] group The constant group.
169
+ # @return [void]
170
+ def check_group_alignment(group)
171
+ columns = group.map { |const| const.loc.operator.column }
172
+ target_column = columns.max
173
+
174
+ group.each do |const|
175
+ current_column = const.loc.operator.column
176
+ next if current_column == target_column
177
+
178
+ add_offense(const.loc.name) do |corrector|
179
+ autocorrect_alignment(corrector, const, target_column)
180
+ end
181
+ end
182
+ end
183
+
184
+ # Auto-correct the alignment of a constant assignment.
185
+ #
186
+ # @param [RuboCop::AST::Corrector] corrector The corrector.
187
+ # @param [RuboCop::AST::Node] const The constant assignment node.
188
+ # @param [Integer] target_column The target column for alignment.
189
+ # @return [void]
190
+ def autocorrect_alignment(corrector, const, target_column)
191
+ name_end = const.loc.name.end_pos
192
+ operator_start = const.loc.operator.begin_pos
193
+ total_spaces = calculate_total_spaces(const, target_column)
194
+
195
+ corrector.replace(
196
+ range_between(name_end, operator_start),
197
+ " " * total_spaces
198
+ )
199
+ end
200
+
201
+ # Calculate total spaces needed for alignment.
202
+ #
203
+ # @param [RuboCop::AST::Node] const The constant assignment node.
204
+ # @param [Integer] target_column The target column for alignment.
205
+ # @return [Integer] The number of spaces (minimum 1).
206
+ def calculate_total_spaces(const, target_column)
207
+ current_column = const.loc.operator.column
208
+ current_spaces = const.loc.operator.begin_pos - const.loc.name.end_pos
209
+ spaces_needed = target_column - current_column
210
+
211
+ [1, current_spaces + spaces_needed].max
212
+ end
213
+
214
+ # Create a source range between two positions.
215
+ #
216
+ # @param [Integer] start_pos The start position.
217
+ # @param [Integer] end_pos The end position.
218
+ # @return [Parser::Source::Range]
219
+ def range_between(start_pos, end_pos)
220
+ Parser::Source::Range.new(
221
+ processed_source.buffer,
222
+ start_pos,
223
+ end_pos
224
+ )
225
+ end
226
+ end
227
+ end
228
+ end
229
+ end
@@ -0,0 +1,219 @@
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
+
32
+ MSG = "Align consecutive `let` declarations at the `{` brace."
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 alignment.
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
+ return if statements.size < 2
70
+
71
+ groups = group_consecutive_lets(statements)
72
+ groups.each { |group| check_group_alignment(group) }
73
+ end
74
+
75
+ # Extract statements from a body node.
76
+ #
77
+ # @param [RuboCop::AST::Node] body The body node.
78
+ # @return [Array<RuboCop::AST::Node>]
79
+ def extract_statements(body)
80
+ if body.begin_type?
81
+ body.children
82
+ else
83
+ [body]
84
+ end
85
+ end
86
+
87
+ # Group consecutive let declarations together.
88
+ #
89
+ # @param [Array<RuboCop::AST::Node>] statements The statements.
90
+ # @return [Array<Array<RuboCop::AST::Node>>] Groups of consecutive lets.
91
+ def group_consecutive_lets(statements)
92
+ groups = []
93
+ current_group = []
94
+ previous_line = nil
95
+
96
+ statements.each do |statement|
97
+ current_group, previous_line = process_statement(statement, current_group, previous_line, groups)
98
+ end
99
+
100
+ finalize_groups(groups, current_group)
101
+ end
102
+
103
+ # Process a single statement for grouping.
104
+ #
105
+ # @param [RuboCop::AST::Node] statement The statement.
106
+ # @param [Array<RuboCop::AST::Node>] current_group The current group.
107
+ # @param [Integer, nil] previous_line The previous line number.
108
+ # @param [Array<Array<RuboCop::AST::Node>>] groups The groups.
109
+ # @return [Array] The updated current_group and previous_line.
110
+ def process_statement(statement, current_group, previous_line, groups)
111
+ if let_declaration?(statement)
112
+ current_group = handle_let(statement, current_group, previous_line, groups)
113
+ else
114
+ save_group_if_valid(groups, current_group)
115
+ current_group = []
116
+ end
117
+ [current_group, statement.loc.last_line]
118
+ end
119
+
120
+ # Handle a let declaration.
121
+ #
122
+ # @param [RuboCop::AST::Node] statement The let declaration.
123
+ # @param [Array<RuboCop::AST::Node>] current_group The current group.
124
+ # @param [Integer, nil] previous_line The previous line number.
125
+ # @param [Array<Array<RuboCop::AST::Node>>] groups The groups.
126
+ # @return [Array<RuboCop::AST::Node>] The updated current group.
127
+ def handle_let(statement, current_group, previous_line, groups)
128
+ if previous_line && statement.loc.line - previous_line > 1
129
+ save_group_if_valid(groups, current_group)
130
+ current_group = []
131
+ end
132
+ current_group << statement
133
+ current_group
134
+ end
135
+
136
+ # Save group if it has multiple let declarations.
137
+ #
138
+ # @param [Array<Array<RuboCop::AST::Node>>] groups The groups.
139
+ # @param [Array<RuboCop::AST::Node>] group The group to potentially save.
140
+ # @return [void]
141
+ def save_group_if_valid(groups, group)
142
+ groups << group if group.size > 1
143
+ end
144
+
145
+ # Finalize groups by adding any remaining valid group.
146
+ #
147
+ # @param [Array<Array<RuboCop::AST::Node>>] groups The groups.
148
+ # @param [Array<RuboCop::AST::Node>] current_group The current group.
149
+ # @return [Array<Array<RuboCop::AST::Node>>] The finalized groups.
150
+ def finalize_groups(groups, current_group)
151
+ save_group_if_valid(groups, current_group)
152
+ groups
153
+ end
154
+
155
+ # Check alignment for a group of let declarations.
156
+ #
157
+ # @param [Array<RuboCop::AST::Node>] group The let group.
158
+ # @return [void]
159
+ def check_group_alignment(group)
160
+ columns = group.map { |let| let.loc.begin.column }
161
+ target_column = columns.max
162
+
163
+ group.each do |let|
164
+ current_column = let.loc.begin.column
165
+ next if current_column == target_column
166
+
167
+ add_offense(let.send_node) do |corrector|
168
+ autocorrect_alignment(corrector, let, target_column)
169
+ end
170
+ end
171
+ end
172
+
173
+ # Auto-correct the alignment of a let declaration.
174
+ #
175
+ # @param [RuboCop::AST::Corrector] corrector The corrector.
176
+ # @param [RuboCop::AST::Node] let The let block node.
177
+ # @param [Integer] target_column The target column for alignment.
178
+ # @return [void]
179
+ def autocorrect_alignment(corrector, let, target_column)
180
+ send_node = let.send_node
181
+ send_end = send_node.source_range.end_pos
182
+ brace_start = let.loc.begin.begin_pos
183
+ total_spaces = calculate_total_spaces(let, target_column)
184
+
185
+ corrector.replace(
186
+ range_between(send_end, brace_start),
187
+ " " * total_spaces
188
+ )
189
+ end
190
+
191
+ # Calculate total spaces needed for alignment.
192
+ #
193
+ # @param [RuboCop::AST::Node] let The let block node.
194
+ # @param [Integer] target_column The target column for alignment.
195
+ # @return [Integer] The number of spaces (minimum 1).
196
+ def calculate_total_spaces(let, target_column)
197
+ current_column = let.loc.begin.column
198
+ current_spaces = let.loc.begin.begin_pos - let.send_node.source_range.end_pos
199
+ spaces_needed = target_column - current_column
200
+
201
+ [1, current_spaces + spaces_needed].max
202
+ end
203
+
204
+ # Create a source range between two positions.
205
+ #
206
+ # @param [Integer] start_pos The start position.
207
+ # @param [Integer] end_pos The end position.
208
+ # @return [Parser::Source::Range]
209
+ def range_between(start_pos, end_pos)
210
+ Parser::Source::Range.new(
211
+ processed_source.buffer,
212
+ start_pos,
213
+ end_pos
214
+ )
215
+ end
216
+ end
217
+ end
218
+ end
219
+ 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,
@@ -9,30 +9,37 @@ 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
- # @example Bad - inline disable directive on a line
13
- # def method
14
- # do_something # rubocop_disable Style/SomeCop (triggers offense)
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 - block disable directive
18
- # # rubocop_disable Style/SomeCop (triggers offense)
19
- # def method
20
- # do_something
21
- # end
22
- # # rubocop_enable Style/SomeCop
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
- # def method
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
32
36
  class NoRubocopDisable < Base
33
- MSG = "Do not use `# rubocop:disable`. Fix the issue or configure globally in `.rubocop.yml`."
37
+ MSG = "Do not disable `%<cops>s`. Fix the issue or configure globally in `.rubocop.yml`."
38
+ MSG_NO_COP = "Do not use `# rubocop:disable`. Fix the issue or configure globally in `.rubocop.yml`."
34
39
 
35
- DISABLE_PATTERN = /\A#\s*rubocop\s*:\s*disable\b/i
40
+ DISABLE_PATTERN = /\A#\s*rubocop\s*:\s*disable\b/i
41
+ COP_NAME_PATTERN = %r{[A-Za-z]+/[A-Za-z0-9]+}
42
+ ALL_PATTERN = /\brubocop\s*:\s*disable\s+all\b/i
36
43
 
37
44
  # Check for rubocop:disable comments.
38
45
  #
@@ -41,7 +48,7 @@ module RuboCop
41
48
  processed_source.comments.each do |comment|
42
49
  next unless disable_comment?(comment)
43
50
 
44
- add_offense(comment)
51
+ check_disabled_cops(comment)
45
52
  end
46
53
  end
47
54
 
@@ -54,6 +61,46 @@ module RuboCop
54
61
  def disable_comment?(comment)
55
62
  DISABLE_PATTERN.match?(comment.text)
56
63
  end
64
+
65
+ # Check disabled cops and flag disallowed ones.
66
+ #
67
+ # @param [Parser::Source::Comment] comment The comment to check.
68
+ # @return [void]
69
+ def check_disabled_cops(comment)
70
+ cops = extract_cop_names(comment.text)
71
+
72
+ if cops.empty?
73
+ add_offense(comment, message: MSG_NO_COP)
74
+ else
75
+ disallowed = cops.reject { |cop| allowed_cop?(cop) }
76
+ return if disallowed.empty?
77
+
78
+ add_offense(comment, message: format(MSG, cops: disallowed.join(", ")))
79
+ end
80
+ end
81
+
82
+ # Extract cop names from a rubocop:disable comment.
83
+ #
84
+ # @param [String] text The comment text.
85
+ # @return [Array<String>]
86
+ def extract_cop_names(text)
87
+ text.scan(COP_NAME_PATTERN)
88
+ end
89
+
90
+ # Check if a cop is in the allowed list.
91
+ #
92
+ # @param [String] cop The cop name.
93
+ # @return [Boolean]
94
+ def allowed_cop?(cop)
95
+ allowed_cops.include?(cop)
96
+ end
97
+
98
+ # Get the list of allowed cops from configuration.
99
+ #
100
+ # @return [Array<String>]
101
+ def allowed_cops
102
+ @allowed_cops ||= Array(cop_config.fetch("AllowedCops", []))
103
+ end
57
104
  end
58
105
  end
59
106
  end
@@ -0,0 +1,101 @@
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
+ corrector.replace(node, replacement)
67
+ end
68
+
69
+ # Build the replacement code for the raise statement.
70
+ #
71
+ # @param [RuboCop::AST::Node] node The if node.
72
+ # @return [String] The replacement code.
73
+ def build_replacement(node)
74
+ condition = build_condition(node)
75
+ raise_source = node.body.source
76
+ base_indent = " " * node.loc.column
77
+ inner_indent = "#{base_indent} "
78
+
79
+ [
80
+ "if #{condition}",
81
+ "#{inner_indent}#{raise_source}",
82
+ "#{base_indent}end"
83
+ ].join("\n")
84
+ end
85
+
86
+ # Build the condition for the if block.
87
+ # For unless, negate the condition. For if, keep it as is.
88
+ #
89
+ # @param [RuboCop::AST::Node] node The if node.
90
+ # @return [String] The condition source.
91
+ def build_condition(node)
92
+ if node.unless?
93
+ "!#{node.condition.source}"
94
+ else
95
+ node.condition.source
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,256 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Vibe
6
+ # Enforces that RSpec stub chains with `.with` or `.and_return` put each
7
+ # chained method on its own line when the line exceeds the max line length.
8
+ #
9
+ # This improves readability by keeping each part of the stub configuration
10
+ # on its own line rather than creating long horizontal lines.
11
+ #
12
+ # The max line length is read from the `Layout/LineLength` cop configuration.
13
+ #
14
+ # @example
15
+ # # bad (when line exceeds max length)
16
+ # allow(Foo).to receive(:bar).with(very_long_argument_name).and_return(result)
17
+ #
18
+ # # good (split when line is too long)
19
+ # allow(Foo).to receive(:bar)
20
+ # .with(very_long_argument_name)
21
+ # .and_return(result)
22
+ #
23
+ # # good - short stubs can stay on one line
24
+ # allow(Foo).to receive(:bar).with(arg).and_return(result)
25
+ #
26
+ # # good - simple stubs without .with or .and_return are fine on one line
27
+ # allow(Foo).to receive(:bar)
28
+ #
29
+ # # good - .and_return directly after receive is allowed
30
+ # allow(Foo).to receive(:bar).and_return(result)
31
+ class RspecStubChainStyle < Base
32
+ extend AutoCorrector
33
+ include SpecFileHelper
34
+
35
+ MSG = "Put each chained stub method on its own line when line is too long."
36
+
37
+ CHAIN_METHODS = %i(
38
+ with
39
+ and_return
40
+ and_raise
41
+ and_throw
42
+ and_yield
43
+ and_call_original
44
+ and_wrap_original
45
+ once
46
+ twice
47
+ thrice
48
+ exactly
49
+ at_least
50
+ at_most
51
+ ordered
52
+ ).freeze
53
+
54
+ RECEIVE_METHODS = %i(receive receive_message_chain receive_messages have_received).freeze
55
+
56
+ # @!method allow_to_receive?(node)
57
+ # Check if node is an allow/expect.to call with a receive chain argument.
58
+ def_node_matcher :allow_to_receive?, <<~PATTERN
59
+ (send
60
+ (send nil? {:allow :expect :allow_any_instance_of :expect_any_instance_of} ...)
61
+ :to
62
+ $send)
63
+ PATTERN
64
+
65
+ # Check send nodes for stub chains that need line breaks.
66
+ #
67
+ # @param [RuboCop::AST::Node] node The send node.
68
+ # @return [void]
69
+ def on_send(node)
70
+ if spec_file?
71
+ allow_to_receive?(node) do |receive_chain|
72
+ check_receive_chain(node, receive_chain)
73
+ end
74
+ end
75
+ end
76
+ alias on_csend on_send
77
+
78
+ private
79
+
80
+ # Check the receive chain for methods that should be on separate lines.
81
+ #
82
+ # @param [RuboCop::AST::Node] to_node The .to node.
83
+ # @param [RuboCop::AST::Node] receive_chain The receive chain argument.
84
+ # @return [void]
85
+ def check_receive_chain(to_node, receive_chain)
86
+ chain = extract_chain(receive_chain)
87
+ receive_index = find_receive_index(chain)
88
+ return unless receive_index
89
+ return unless chain_has_with?(chain)
90
+ return unless line_exceeds_max_length?(to_node)
91
+
92
+ methods = chain[(receive_index + 1)..]
93
+ check_methods_alignment(to_node, chain[receive_index], methods)
94
+ end
95
+
96
+ # Check if the line containing the node exceeds the max line length.
97
+ #
98
+ # @param [RuboCop::AST::Node] node The node to check.
99
+ # @return [Boolean]
100
+ def line_exceeds_max_length?(node)
101
+ line_number = node.loc.line
102
+ line = processed_source.lines[line_number - 1]
103
+
104
+ line.length > max_line_length
105
+ end
106
+
107
+ # Get the configured max line length from Layout/LineLength.
108
+ #
109
+ # @return [Integer]
110
+ def max_line_length
111
+ config.for_cop("Layout/LineLength")["Max"] || 120
112
+ end
113
+
114
+ # Extract the method chain from a node.
115
+ #
116
+ # @param [RuboCop::AST::Node] node The send node.
117
+ # @return [Array<RuboCop::AST::Node>]
118
+ def extract_chain(node)
119
+ chain = []
120
+ current = node
121
+
122
+ while current&.send_type?
123
+ chain.unshift(current)
124
+ current = current.receiver
125
+ end
126
+
127
+ chain
128
+ end
129
+
130
+ # Find the index of the receive method in the chain.
131
+ #
132
+ # @param [Array<RuboCop::AST::Node>] chain The method chain.
133
+ # @return [Integer, nil]
134
+ def find_receive_index(chain)
135
+ chain.index { |n| RECEIVE_METHODS.include?(n.method_name) }
136
+ end
137
+
138
+ # Check if the chain includes a .with call.
139
+ #
140
+ # @param [Array<RuboCop::AST::Node>] chain The method chain.
141
+ # @return [Boolean]
142
+ def chain_has_with?(chain)
143
+ chain.any? { |n| n.method?(:with) }
144
+ end
145
+
146
+ # Check alignment of methods after receive.
147
+ #
148
+ # @param [RuboCop::AST::Node] to_node The .to node.
149
+ # @param [RuboCop::AST::Node] receive_node The receive node.
150
+ # @param [Array<RuboCop::AST::Node>] methods The methods to check.
151
+ # @return [void]
152
+ def check_methods_alignment(to_node, receive_node, methods)
153
+ previous_node = receive_node
154
+ is_first = true
155
+
156
+ methods.each do |method_node|
157
+ if should_flag?(previous_node, method_node, is_first) && chainable_method?(method_node)
158
+ register_offense(method_node, to_node)
159
+ end
160
+ previous_node = method_node
161
+ is_first = false
162
+ end
163
+ end
164
+
165
+ # Check if the method should be flagged.
166
+ #
167
+ # @param [RuboCop::AST::Node] previous_node The previous node.
168
+ # @param [RuboCop::AST::Node] method_node The method node.
169
+ # @param [Boolean] is_first Whether this is the first method after receive.
170
+ # @return [Boolean]
171
+ def should_flag?(previous_node, method_node, is_first)
172
+ reference_line = is_first ? previous_node.loc.last_line : previous_node.loc.selector.line
173
+ reference_line == method_node.loc.selector.line
174
+ end
175
+
176
+ # Check if the method is one we care about for chaining.
177
+ #
178
+ # @param [RuboCop::AST::Node] node The method node.
179
+ # @return [Boolean]
180
+ def chainable_method?(node)
181
+ CHAIN_METHODS.include?(node.method_name)
182
+ end
183
+
184
+ # Register an offense for a method that should be on its own line.
185
+ #
186
+ # @param [RuboCop::AST::Node] method_node The method node.
187
+ # @param [RuboCop::AST::Node] previous_node The previous node in chain.
188
+ # @return [void]
189
+ def register_offense(method_node, previous_node)
190
+ add_offense(method_node.loc.selector) do |corrector|
191
+ autocorrect_chain(corrector, method_node, previous_node)
192
+ end
193
+ end
194
+
195
+ # Auto-correct by inserting a newline before the method.
196
+ #
197
+ # @param [RuboCop::Cop::Corrector] corrector The corrector.
198
+ # @param [RuboCop::AST::Node] method_node The method to move.
199
+ # @param [RuboCop::AST::Node] to_node The .to node for indentation.
200
+ # @return [void]
201
+ def autocorrect_chain(corrector, method_node, to_node)
202
+ dot_range = method_node.loc.dot
203
+ indentation = calculate_indentation(to_node)
204
+ replacement_start = find_replacement_start(method_node)
205
+
206
+ corrector.replace(
207
+ range_between(replacement_start, dot_range.begin_pos),
208
+ "\n#{indentation}"
209
+ )
210
+ end
211
+
212
+ # Find the starting position for the replacement.
213
+ #
214
+ # @param [RuboCop::AST::Node] method_node The method node.
215
+ # @return [Integer]
216
+ def find_replacement_start(method_node)
217
+ method_node.receiver.source_range.end_pos
218
+ end
219
+
220
+ # Calculate the indentation for the new line.
221
+ #
222
+ # @param [RuboCop::AST::Node] node The node to base indentation on.
223
+ # @return [String]
224
+ def calculate_indentation(node)
225
+ base_column = find_chain_start_column(node)
226
+ " " * (base_column + 2)
227
+ end
228
+
229
+ # Find the starting column of the chain.
230
+ #
231
+ # @param [RuboCop::AST::Node] node A node in the chain.
232
+ # @return [Integer]
233
+ def find_chain_start_column(node)
234
+ current = node
235
+
236
+ current = current.receiver while current.receiver&.send_type?
237
+
238
+ current.loc.column
239
+ end
240
+
241
+ # Create a source range between two positions.
242
+ #
243
+ # @param [Integer] start_pos The start position.
244
+ # @param [Integer] end_pos The end position.
245
+ # @return [Parser::Source::Range]
246
+ def range_between(start_pos, end_pos)
247
+ Parser::Source::Range.new(
248
+ processed_source.buffer,
249
+ start_pos,
250
+ end_pos
251
+ )
252
+ end
253
+ end
254
+ end
255
+ end
256
+ end
@@ -5,6 +5,8 @@ require_relative "vibe/mixin/spec_file_helper"
5
5
  require_relative "vibe/blank_line_before_expectation"
6
6
  require_relative "vibe/class_organization"
7
7
  require_relative "vibe/consecutive_assignment_alignment"
8
+ require_relative "vibe/consecutive_constant_alignment"
9
+ require_relative "vibe/consecutive_let_alignment"
8
10
  require_relative "vibe/describe_block_order"
9
11
  require_relative "vibe/is_expected_one_liner"
10
12
  require_relative "vibe/no_assigns_attribute_testing"
@@ -12,4 +14,6 @@ require_relative "vibe/no_rubocop_disable"
12
14
  require_relative "vibe/no_skipped_tests"
13
15
  require_relative "vibe/no_unless_guard_clause"
14
16
  require_relative "vibe/prefer_one_liner_expectation"
17
+ require_relative "vibe/raise_unless_block"
18
+ require_relative "vibe/rspec_stub_chain_style"
15
19
  require_relative "vibe/service_call_method"
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RuboCop
4
4
  module Vibe
5
- VERSION = "0.1.0"
5
+ VERSION = "0.2.0"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubocop-vibe
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tristan Dunn
@@ -90,6 +90,8 @@ files:
90
90
  - lib/rubocop/cop/vibe/blank_line_before_expectation.rb
91
91
  - lib/rubocop/cop/vibe/class_organization.rb
92
92
  - lib/rubocop/cop/vibe/consecutive_assignment_alignment.rb
93
+ - lib/rubocop/cop/vibe/consecutive_constant_alignment.rb
94
+ - lib/rubocop/cop/vibe/consecutive_let_alignment.rb
93
95
  - lib/rubocop/cop/vibe/describe_block_order.rb
94
96
  - lib/rubocop/cop/vibe/is_expected_one_liner.rb
95
97
  - lib/rubocop/cop/vibe/mixin/spec_file_helper.rb
@@ -98,6 +100,8 @@ files:
98
100
  - lib/rubocop/cop/vibe/no_skipped_tests.rb
99
101
  - lib/rubocop/cop/vibe/no_unless_guard_clause.rb
100
102
  - lib/rubocop/cop/vibe/prefer_one_liner_expectation.rb
103
+ - lib/rubocop/cop/vibe/raise_unless_block.rb
104
+ - lib/rubocop/cop/vibe/rspec_stub_chain_style.rb
101
105
  - lib/rubocop/cop/vibe/service_call_method.rb
102
106
  - lib/rubocop/cop/vibe_cops.rb
103
107
  - lib/rubocop/vibe.rb