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,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Vibe
6
+ # Enforces using `do...end` syntax instead of braces for RSpec `before` blocks.
7
+ #
8
+ # @example
9
+ # # bad
10
+ # before { setup_data }
11
+ #
12
+ # # bad
13
+ # before(:each) { setup_data }
14
+ #
15
+ # # good
16
+ # before do
17
+ # setup_data
18
+ # end
19
+ #
20
+ # # good
21
+ # before(:each) do
22
+ # setup_data
23
+ # end
24
+ class RspecBeforeBlockStyle < Base
25
+ extend AutoCorrector
26
+ include SpecFileHelper
27
+
28
+ MSG = "Use `do...end` block syntax instead of braces for `%<method>s` blocks."
29
+
30
+ HOOK_METHODS = %i(before after around).freeze
31
+
32
+ # Check block nodes for brace-style hooks.
33
+ #
34
+ # @param [RuboCop::AST::Node] node The block node.
35
+ # @return [void]
36
+ def on_block(node)
37
+ return unless spec_file?
38
+ return unless hook_block?(node)
39
+ return unless node.braces?
40
+
41
+ method_name = node.method_name
42
+
43
+ add_offense(node.loc.begin, message: format(MSG, method: method_name)) do |corrector|
44
+ autocorrect(corrector, node)
45
+ end
46
+ end
47
+ alias on_numblock on_block
48
+
49
+ private
50
+
51
+ # Check if the block is a hook block (before, after, around).
52
+ #
53
+ # @param [RuboCop::AST::Node] node The block node.
54
+ # @return [Boolean]
55
+ def hook_block?(node)
56
+ send_node = node.send_node
57
+
58
+ send_node.receiver.nil? && HOOK_METHODS.include?(send_node.method_name)
59
+ end
60
+
61
+ # Autocorrect the offense by converting braces to do...end.
62
+ #
63
+ # @param [RuboCop::Cop::Corrector] corrector The corrector.
64
+ # @param [RuboCop::AST::Node] node The block node.
65
+ # @return [void]
66
+ def autocorrect(corrector, node)
67
+ base_indent = " " * node.loc.column
68
+ replacement = build_replacement(node, base_indent)
69
+
70
+ corrector.replace(block_range(node), replacement)
71
+ end
72
+
73
+ # Build the replacement string for the block.
74
+ #
75
+ # @param [RuboCop::AST::Node] node The block node.
76
+ # @param [String] base_indent The base indentation.
77
+ # @return [String]
78
+ def build_replacement(node, base_indent)
79
+ parts = ["do"]
80
+
81
+ if node.arguments.source_range
82
+ parts << " #{node.arguments.source}"
83
+ end
84
+
85
+ if node.body
86
+ body_source = format_body(node.body, "#{base_indent} ")
87
+ parts << "\n#{body_source}"
88
+ end
89
+
90
+ parts << "\n#{base_indent}end"
91
+
92
+ parts.join
93
+ end
94
+
95
+ # Get the range from the opening brace to the closing brace.
96
+ #
97
+ # @param [RuboCop::AST::Node] node The block node.
98
+ # @return [Parser::Source::Range]
99
+ def block_range(node)
100
+ node.loc.begin.join(node.loc.end)
101
+ end
102
+
103
+ # Format the body with proper indentation.
104
+ #
105
+ # @param [RuboCop::AST::Node] body The body node.
106
+ # @param [String] indent The indentation string.
107
+ # @return [String]
108
+ def format_body(body, indent)
109
+ body.source.lines.map { |line| "#{indent}#{line.strip}" }.join("\n")
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,260 @@
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
+
89
+ return unless receive_index
90
+ return unless chain_has_with?(chain)
91
+ return unless line_exceeds_max_length?(to_node)
92
+
93
+ methods = chain[(receive_index + 1)..]
94
+
95
+ check_methods_alignment(to_node, chain[receive_index], methods)
96
+ end
97
+
98
+ # Check if the line containing the node exceeds the max line length.
99
+ #
100
+ # @param [RuboCop::AST::Node] node The node to check.
101
+ # @return [Boolean]
102
+ def line_exceeds_max_length?(node)
103
+ line_number = node.loc.line
104
+ line = processed_source.lines[line_number - 1]
105
+
106
+ line.length > max_line_length
107
+ end
108
+
109
+ # Get the configured max line length from Layout/LineLength.
110
+ #
111
+ # @return [Integer]
112
+ def max_line_length
113
+ config.for_cop("Layout/LineLength")["Max"] || 120
114
+ end
115
+
116
+ # Extract the method chain from a node.
117
+ #
118
+ # @param [RuboCop::AST::Node] node The send node.
119
+ # @return [Array<RuboCop::AST::Node>]
120
+ def extract_chain(node)
121
+ chain = []
122
+ current = node
123
+
124
+ while current&.send_type?
125
+ chain.unshift(current)
126
+ current = current.receiver
127
+ end
128
+
129
+ chain
130
+ end
131
+
132
+ # Find the index of the receive method in the chain.
133
+ #
134
+ # @param [Array<RuboCop::AST::Node>] chain The method chain.
135
+ # @return [Integer, nil]
136
+ def find_receive_index(chain)
137
+ chain.index { |n| RECEIVE_METHODS.include?(n.method_name) }
138
+ end
139
+
140
+ # Check if the chain includes a .with call.
141
+ #
142
+ # @param [Array<RuboCop::AST::Node>] chain The method chain.
143
+ # @return [Boolean]
144
+ def chain_has_with?(chain)
145
+ chain.any? { |n| n.method?(:with) }
146
+ end
147
+
148
+ # Check alignment of methods after receive.
149
+ #
150
+ # @param [RuboCop::AST::Node] to_node The .to node.
151
+ # @param [RuboCop::AST::Node] receive_node The receive node.
152
+ # @param [Array<RuboCop::AST::Node>] methods The methods to check.
153
+ # @return [void]
154
+ def check_methods_alignment(to_node, receive_node, methods)
155
+ previous_node = receive_node
156
+ is_first = true
157
+
158
+ methods.each do |method_node|
159
+ if should_flag?(previous_node, method_node, is_first) && chainable_method?(method_node)
160
+ register_offense(method_node, to_node)
161
+ end
162
+ previous_node = method_node
163
+ is_first = false
164
+ end
165
+ end
166
+
167
+ # Check if the method should be flagged.
168
+ #
169
+ # @param [RuboCop::AST::Node] previous_node The previous node.
170
+ # @param [RuboCop::AST::Node] method_node The method node.
171
+ # @param [Boolean] is_first Whether this is the first method after receive.
172
+ # @return [Boolean]
173
+ def should_flag?(previous_node, method_node, is_first)
174
+ reference_line = is_first ? previous_node.loc.last_line : previous_node.loc.selector.line
175
+
176
+ reference_line == method_node.loc.selector.line
177
+ end
178
+
179
+ # Check if the method is one we care about for chaining.
180
+ #
181
+ # @param [RuboCop::AST::Node] node The method node.
182
+ # @return [Boolean]
183
+ def chainable_method?(node)
184
+ CHAIN_METHODS.include?(node.method_name)
185
+ end
186
+
187
+ # Register an offense for a method that should be on its own line.
188
+ #
189
+ # @param [RuboCop::AST::Node] method_node The method node.
190
+ # @param [RuboCop::AST::Node] previous_node The previous node in chain.
191
+ # @return [void]
192
+ def register_offense(method_node, previous_node)
193
+ add_offense(method_node.loc.selector) do |corrector|
194
+ autocorrect_chain(corrector, method_node, previous_node)
195
+ end
196
+ end
197
+
198
+ # Auto-correct by inserting a newline before the method.
199
+ #
200
+ # @param [RuboCop::Cop::Corrector] corrector The corrector.
201
+ # @param [RuboCop::AST::Node] method_node The method to move.
202
+ # @param [RuboCop::AST::Node] to_node The .to node for indentation.
203
+ # @return [void]
204
+ def autocorrect_chain(corrector, method_node, to_node)
205
+ dot_range = method_node.loc.dot
206
+ indentation = calculate_indentation(to_node)
207
+ replacement_start = find_replacement_start(method_node)
208
+
209
+ corrector.replace(
210
+ range_between(replacement_start, dot_range.begin_pos),
211
+ "\n#{indentation}"
212
+ )
213
+ end
214
+
215
+ # Find the starting position for the replacement.
216
+ #
217
+ # @param [RuboCop::AST::Node] method_node The method node.
218
+ # @return [Integer]
219
+ def find_replacement_start(method_node)
220
+ method_node.receiver.source_range.end_pos
221
+ end
222
+
223
+ # Calculate the indentation for the new line.
224
+ #
225
+ # @param [RuboCop::AST::Node] node The node to base indentation on.
226
+ # @return [String]
227
+ def calculate_indentation(node)
228
+ base_column = find_chain_start_column(node)
229
+
230
+ " " * (base_column + 2)
231
+ end
232
+
233
+ # Find the starting column of the chain.
234
+ #
235
+ # @param [RuboCop::AST::Node] node A node in the chain.
236
+ # @return [Integer]
237
+ def find_chain_start_column(node)
238
+ current = node
239
+
240
+ current = current.receiver while current.receiver&.send_type?
241
+
242
+ current.loc.column
243
+ end
244
+
245
+ # Create a source range between two positions.
246
+ #
247
+ # @param [Integer] start_pos The start position.
248
+ # @param [Integer] end_pos The end position.
249
+ # @return [Parser::Source::Range]
250
+ def range_between(start_pos, end_pos)
251
+ Parser::Source::Range.new(
252
+ processed_source.buffer,
253
+ start_pos,
254
+ end_pos
255
+ )
256
+ end
257
+ end
258
+ end
259
+ end
260
+ end
@@ -1,15 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "vibe/mixin/alignment_helpers"
3
4
  require_relative "vibe/mixin/spec_file_helper"
4
5
 
6
+ require_relative "vibe/blank_line_after_assignment"
5
7
  require_relative "vibe/blank_line_before_expectation"
6
8
  require_relative "vibe/class_organization"
7
9
  require_relative "vibe/consecutive_assignment_alignment"
10
+ require_relative "vibe/consecutive_constant_alignment"
11
+ require_relative "vibe/consecutive_indexed_assignment_alignment"
12
+ require_relative "vibe/consecutive_let_alignment"
8
13
  require_relative "vibe/describe_block_order"
14
+ require_relative "vibe/explicit_return_conditional"
9
15
  require_relative "vibe/is_expected_one_liner"
16
+ require_relative "vibe/multiline_hash_argument_style"
10
17
  require_relative "vibe/no_assigns_attribute_testing"
18
+ require_relative "vibe/no_compound_conditions"
11
19
  require_relative "vibe/no_rubocop_disable"
12
20
  require_relative "vibe/no_skipped_tests"
13
21
  require_relative "vibe/no_unless_guard_clause"
14
22
  require_relative "vibe/prefer_one_liner_expectation"
23
+ require_relative "vibe/raise_unless_block"
24
+ require_relative "vibe/rspec_before_block_style"
25
+ require_relative "vibe/rspec_stub_chain_style"
15
26
  require_relative "vibe/service_call_method"
@@ -10,10 +10,10 @@ module RuboCop
10
10
  # @return [LintRoller::About] Information about the plug-in.
11
11
  def about
12
12
  LintRoller::About.new(
13
- name: "rubocop-vibe",
14
- version: VERSION,
13
+ description: "A set of custom cops to use on AI generated code.",
15
14
  homepage: "https://github.com/tristandunn/rubocop-vibe",
16
- description: "A set of custom cops to use on AI generated code."
15
+ name: "rubocop-vibe",
16
+ version: VERSION
17
17
  )
18
18
  end
19
19
 
@@ -23,8 +23,8 @@ module RuboCop
23
23
  # @return [LintRoller::Rules] The rules for this plug-in.
24
24
  def rules(_context)
25
25
  LintRoller::Rules.new(
26
- type: :path,
27
26
  config_format: :rubocop,
27
+ type: :path,
28
28
  value: Pathname.new(__dir__).join("../../../config/default.yml")
29
29
  )
30
30
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RuboCop
4
4
  module Vibe
5
- VERSION = "0.1.0"
5
+ VERSION = "0.3.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.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tristan Dunn
@@ -87,17 +87,28 @@ extra_rdoc_files: []
87
87
  files:
88
88
  - config/default.yml
89
89
  - lib/rubocop-vibe.rb
90
+ - lib/rubocop/cop/vibe/blank_line_after_assignment.rb
90
91
  - lib/rubocop/cop/vibe/blank_line_before_expectation.rb
91
92
  - lib/rubocop/cop/vibe/class_organization.rb
92
93
  - lib/rubocop/cop/vibe/consecutive_assignment_alignment.rb
94
+ - lib/rubocop/cop/vibe/consecutive_constant_alignment.rb
95
+ - lib/rubocop/cop/vibe/consecutive_indexed_assignment_alignment.rb
96
+ - lib/rubocop/cop/vibe/consecutive_let_alignment.rb
93
97
  - lib/rubocop/cop/vibe/describe_block_order.rb
98
+ - lib/rubocop/cop/vibe/explicit_return_conditional.rb
94
99
  - lib/rubocop/cop/vibe/is_expected_one_liner.rb
100
+ - lib/rubocop/cop/vibe/mixin/alignment_helpers.rb
95
101
  - lib/rubocop/cop/vibe/mixin/spec_file_helper.rb
102
+ - lib/rubocop/cop/vibe/multiline_hash_argument_style.rb
96
103
  - lib/rubocop/cop/vibe/no_assigns_attribute_testing.rb
104
+ - lib/rubocop/cop/vibe/no_compound_conditions.rb
97
105
  - lib/rubocop/cop/vibe/no_rubocop_disable.rb
98
106
  - lib/rubocop/cop/vibe/no_skipped_tests.rb
99
107
  - lib/rubocop/cop/vibe/no_unless_guard_clause.rb
100
108
  - lib/rubocop/cop/vibe/prefer_one_liner_expectation.rb
109
+ - lib/rubocop/cop/vibe/raise_unless_block.rb
110
+ - lib/rubocop/cop/vibe/rspec_before_block_style.rb
111
+ - lib/rubocop/cop/vibe/rspec_stub_chain_style.rb
101
112
  - lib/rubocop/cop/vibe/service_call_method.rb
102
113
  - lib/rubocop/cop/vibe_cops.rb
103
114
  - lib/rubocop/vibe.rb
@@ -125,7 +136,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
125
136
  - !ruby/object:Gem::Version
126
137
  version: '0'
127
138
  requirements: []
128
- rubygems_version: 4.0.3
139
+ rubygems_version: 4.0.4
129
140
  specification_version: 4
130
141
  summary: A set of custom cops to use on AI generated code.
131
142
  test_files: []