rubocop-vibe 0.6.0 → 0.7.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: 32efe500f7fb50e4005a4ef34081405eee947bc058b96c684e60d3edb1095976
4
- data.tar.gz: 79b4d44786594d509536e369756ead61b0cd34cd54cf288f857b4fa04e90f0a5
3
+ metadata.gz: 51eb5df4d2c3cea10d5c91ffdda0196ffd78d4863f677209daf4485fb2982386
4
+ data.tar.gz: 90ead0406cb08cf50c7b894b3f7031480cf04736cf9ae790496b55d175489deb
5
5
  SHA512:
6
- metadata.gz: 62fb11ebb915d21241f1c0da9b6684564e457aeaa0469ce2fc5ec708bfaf1754f8b4f85dfc7167b7ea68b9e498424d6feb3e6afb57673f28a2921e88fba56f3d
7
- data.tar.gz: 10bdf89f1dcfd15dbdfe06d293d3a6633193ecf953d50133be333d50b70b3ed7f84f081f6d40c4cbafd3fa68ae50839958ad1fe92eafddd44151b3f1c068e090
6
+ metadata.gz: 4a51750ef0ed7e271fcc054e4492506e9296fad7cdd605543e9e7e1baa20f7a7be4eb7fcf5429551417cd21422326abb74735603474579c37f767478ecb4e47d
7
+ data.tar.gz: 900cd97045e70e6fbedb103245d20b5453f85a59b6eade6c1df1efd783407024289f7f611e78f1ee10739bb322a3a8fe3e796d990930bf9dc0b96a27f84c7830
data/config/default.yml CHANGED
@@ -160,6 +160,12 @@ Vibe/LetOrder:
160
160
  SafeAutoCorrect: true
161
161
  VersionAdded: "0.4.0"
162
162
 
163
+ Vibe/ModuleOrganization:
164
+ Description: "Enforces consistent organization of module definitions."
165
+ Enabled: true
166
+ SafeAutoCorrect: false
167
+ VersionAdded: "0.7.0"
168
+
163
169
  Vibe/MultilineHashArgumentStyle:
164
170
  Description: "Enforces one-per-line and alphabetical ordering for hash arguments in multiline method calls."
165
171
  Enabled: true
@@ -183,6 +189,13 @@ Vibe/NoRubocopDisable:
183
189
  AllowedCops: []
184
190
  VersionAdded: "0.1.0"
185
191
 
192
+ Vibe/NoSingleCharacterVariableNames:
193
+ Description: "Disallows single character variable names."
194
+ Enabled: true
195
+ AllowedNames:
196
+ - "_"
197
+ VersionAdded: "0.7.0"
198
+
186
199
  Vibe/NoSkippedTests:
187
200
  Description: "Enforces that tests are not skipped or marked as pending."
188
201
  Enabled: true
@@ -204,11 +204,11 @@ module RuboCop
204
204
  # @return [RuboCop::AST::Node, nil]
205
205
  def leftmost_receiver(node)
206
206
  return node if node.lvar_type?
207
- return unless node.send_type?
207
+ return unless node.call_type?
208
208
 
209
209
  current = node
210
210
 
211
- current = current.receiver while current.send_type? && current.receiver
211
+ current = current.receiver while current.call_type? && current.receiver
212
212
 
213
213
  current
214
214
  end
@@ -39,9 +39,9 @@ module RuboCop
39
39
  # end
40
40
  class ClassOrganization < Base
41
41
  extend AutoCorrector
42
+ include BodyOrganization
42
43
 
43
- ASSOCIATIONS = %i(belongs_to has_one has_many has_and_belongs_to_many).freeze
44
- ATTR_SORT_ORDER = { attr_accessor: "!0", attr_reader: "!1", attr_writer: "!2" }.freeze
44
+ ASSOCIATIONS = %i(belongs_to has_one has_many has_and_belongs_to_many).freeze
45
45
  CALLBACKS = %i(
46
46
  before_validation after_validation
47
47
  before_save after_save around_save
@@ -86,288 +86,78 @@ module RuboCop
86
86
  validates_length_of validates_numericality_of validates_presence_of
87
87
  validates_size_of validates_uniqueness_of validates_associated
88
88
  ).freeze
89
- VISIBILITY_CATEGORIES = {
90
- protected: :protected_methods,
91
- private: :private_methods,
92
- public: :instance_methods
93
- }.freeze
94
-
95
- # Check and register violations.
96
- #
97
- # @param [RuboCop::AST::Node] class_node The class node.
98
- # @param [Array<Hash>] elements The elements.
99
- # @param [Boolean] is_model Whether this is a Rails model.
100
- # @return [void]
101
- def check_violations(class_node, elements, is_model)
102
- violations = find_violations(elements)
103
-
104
- return if violations.empty?
105
-
106
- message = is_model ? MODEL_MSG : CLASS_MSG
107
-
108
- add_offense(violations.first[:node], message: message) do |corrector|
109
- autocorrect(corrector, class_node, elements)
110
- end
111
- end
112
89
 
113
90
  # Check class nodes for organization.
114
91
  #
115
92
  # @param [RuboCop::AST::Node] node The class node.
116
93
  # @return [void]
117
94
  def on_class(node)
118
- is_model = rails_model?(node)
119
- is_controller = rails_controller?(node)
95
+ @is_model = rails_model?(node)
96
+ @is_controller = rails_controller?(node)
120
97
 
121
- return if !is_model && !is_controller && !node.body
98
+ return if !@is_model && !@is_controller && !node.body
99
+ return unless node.body
122
100
 
123
- elements = extract_elements(node, is_model: is_model, is_controller: is_controller)
101
+ elements = collect_class_elements(node.body)
124
102
 
125
103
  return if elements.size < 2
126
104
 
127
- check_violations(node, elements, is_model)
105
+ check_body_violations(node, elements)
128
106
  end
129
107
 
130
108
  private
131
109
 
132
- # Get category for attr methods based on visibility.
133
- #
134
- # @param [Symbol] visibility The current visibility.
135
- # @return [Symbol]
136
- def attr_category(visibility)
137
- if visibility == :public
138
- :attr_methods
139
- else
140
- visibility_method_category(visibility)
141
- end
142
- end
143
-
144
- # Check if node is an attr method.
145
- #
146
- # @param [RuboCop::AST::Node] node The node to check.
147
- # @return [Boolean]
148
- def attr_method?(node)
149
- node.send_type? && ATTR_SORT_ORDER.key?(node.method_name)
150
- end
151
-
152
- # Auto-correct by sorting elements within each visibility section.
153
- #
154
- # @param [RuboCop::AST::Corrector] corrector The corrector.
155
- # @param [RuboCop::AST::Node] _class_node The class node.
156
- # @param [Array<Hash>] elements The list of elements.
157
- # @return [void]
158
- def autocorrect(corrector, _class_node, elements)
159
- elements.group_by { |e| e[:visibility] }.each_value do |section|
160
- sorted = sort_elements(section)
161
-
162
- next if sorted == section
163
-
164
- autocorrect_group(corrector, section, sorted)
165
- end
166
- end
167
-
168
- # Auto-correct a single consecutive group.
169
- #
170
- # @param [RuboCop::AST::Corrector] corrector The corrector.
171
- # @param [Array<Hash>] group The original elements.
172
- # @param [Array<Hash>] sorted The sorted elements.
173
- # @return [void]
174
- def autocorrect_group(corrector, group, sorted)
175
- sorted.each_with_index do |element, idx|
176
- original = group[idx]
177
-
178
- next if element[:node] == original[:node]
179
-
180
- corrector.replace(
181
- node_range_with_comments(original[:node]),
182
- source_with_comments(element[:node])
183
- )
184
- end
185
- end
186
-
187
- # Build element hash for a node.
188
- #
189
- # @param [RuboCop::AST::Node] child The node.
190
- # @param [Symbol] visibility The current visibility.
191
- # @param [Integer] index The original index.
192
- # @param [Boolean] is_model Whether this is a Rails model.
193
- # @param [Boolean] is_controller Whether this is a Rails controller.
194
- # @return [Hash]
195
- # @return [nil]
196
- def build_element(child, visibility, index, is_model, is_controller)
197
- return unless categorizable?(child)
198
-
199
- category = categorize_node(child, visibility, is_model)
200
-
201
- return unless category
202
-
203
- # Skip public instance methods in controllers (Rails/ActionOrder handles them).
204
- return if is_controller && category == :instance_methods && visibility == :public
205
-
206
- element_hash(child, category, visibility, index, is_model)
207
- end
208
-
209
- # Check if node should be categorized.
210
- #
211
- # @param [RuboCop::AST::Node] node The node to check.
212
- # @return [Boolean]
213
- def categorizable?(node)
214
- node.type?(:send, :any_def, :casgn)
215
- end
216
-
217
- # Categorize a node.
110
+ # Categorize send nodes for classes.
218
111
  #
219
112
  # @param [RuboCop::AST::Node] node The node to categorize.
220
- # @param [Symbol] visibility The current visibility.
221
- # @param [Boolean] is_model Whether this is a Rails model.
113
+ # @param [Symbol] _visibility The current visibility.
222
114
  # @return [Symbol]
223
- # @return [nil] When node doesn't fit a category.
224
- def categorize_node(node, visibility, is_model)
225
- return method_category(node, visibility) if node.any_def_type?
226
- return :constants if node.casgn_type?
115
+ # @return [nil]
116
+ def categorize_send_node(node, _visibility)
117
+ return :concerns if node.method?(:include)
118
+ return nil unless @is_model
227
119
 
228
- return nil if visibility_modifier?(node)
229
- return attr_category(visibility) if attr_method?(node)
120
+ return :associations if ASSOCIATIONS.include?(node.method_name)
121
+ return :validations if validation_method?(node)
122
+ return :callbacks if CALLBACKS.include?(node.method_name)
123
+ return :scopes if node.method?(:scope)
230
124
 
231
- send_category(node, visibility, is_model)
125
+ nil
232
126
  end
233
127
 
234
- # Collect elements from body nodes.
128
+ # Collect elements from class body with controller filtering.
235
129
  #
236
130
  # @param [RuboCop::AST::Node] body The body node.
237
- # @param [Boolean] is_model Whether this is a Rails model.
238
- # @param [Boolean] is_controller Whether this is a Rails controller.
239
131
  # @return [Array<Hash>] Array of element hashes.
240
- def collect_elements(body, is_model, is_controller)
132
+ def collect_class_elements(body)
241
133
  visibility = :public
242
134
  elements = []
243
- index = 0
244
135
 
245
136
  process_body_nodes(body).each do |child|
246
137
  visibility = child.method_name if visibility_modifier?(child)
138
+ element = build_body_element(child, visibility, elements.size)
247
139
 
248
- element = build_element(child, visibility, index, is_model, is_controller)
140
+ next if element.nil? || controller_public_instance_method?(element, visibility)
249
141
 
250
- elements << element and index += 1 if element
142
+ elements << element
251
143
  end
252
144
 
253
145
  elements
254
146
  end
255
147
 
256
- # Get comments immediately before a node.
257
- #
258
- # @param [RuboCop::AST::Node] node The node.
259
- # @return [Array<Parser::Source::Comment>] Consecutive comments before node.
260
- def comments_before(node)
261
- consecutive = []
262
- expected_line = node.first_line - 1
263
-
264
- while (comment = comments_by_line[expected_line])
265
- consecutive.unshift(comment)
266
- expected_line -= 1
267
- end
268
-
269
- consecutive
270
- end
271
-
272
- # Get comments indexed by line number for fast lookup.
273
- #
274
- # @return [Hash<Integer, Parser::Source::Comment>] Comments keyed by line.
275
- def comments_by_line
276
- @comments_by_line ||= processed_source.comments.to_h { |c| [c.location.line, c] }
148
+ # Check if element is a public instance method in a controller.
149
+ def controller_public_instance_method?(element, visibility)
150
+ @is_controller && element[:category] == :instance_methods && visibility == :public
277
151
  end
278
152
 
279
- # Create element hash.
153
+ # Get the priority hash for the current context.
280
154
  #
281
- # @param [RuboCop::AST::Node] node The node.
282
- # @param [Symbol] category The category.
283
- # @param [Symbol] visibility The visibility.
284
- # @param [Integer] index The original index.
285
- # @param [Boolean] is_model Whether this is a Rails model.
286
155
  # @return [Hash]
287
- def element_hash(node, category, visibility, index, is_model)
288
- {
289
- node: node,
290
- category: category,
291
- visibility: visibility,
292
- original_index: index,
293
- priority: priority_for(category, node, is_model),
294
- sort_key: sort_key_for(category, node)
295
- }
296
- end
297
-
298
- # Extract and categorize elements from the class.
299
- #
300
- # @param [RuboCop::AST::Node] node The class node.
301
- # @param [Boolean] is_model Whether this is a Rails model.
302
- # @param [Boolean] is_controller Whether this is a Rails controller.
303
- # @return [Array<Hash>] Array of element info.
304
- def extract_elements(node, is_model:, is_controller: false)
305
- if node.body
306
- collect_elements(node.body, is_model, is_controller)
156
+ def priorities
157
+ if @is_model
158
+ MODEL_PRIORITIES
307
159
  else
308
- []
309
- end
310
- end
311
-
312
- # Find elements that violate ordering.
313
- #
314
- # @param [Array<Hash>] elements The list of elements.
315
- # @return [Array<Hash>] Elements that violate ordering.
316
- def find_violations(elements)
317
- violations = []
318
-
319
- elements.each_cons(2) do |current, following|
320
- violations << following if violates_order?(current, following)
321
- end
322
-
323
- violations.uniq
324
- end
325
-
326
- # Categorize method nodes.
327
- #
328
- # @param [RuboCop::AST::Node] node The node to categorize.
329
- # @param [Symbol] visibility The current visibility.
330
- # @return [Symbol]
331
- def method_category(node, visibility)
332
- return :class_methods if node.defs_type?
333
- return :initialize if node.method?(:initialize) && visibility == :public
334
-
335
- visibility_method_category(visibility)
336
- end
337
-
338
- # Get the source range of a node including preceding comments.
339
- #
340
- # @param [RuboCop::AST::Node] node The node.
341
- # @return [Parser::Source::Range]
342
- def node_range_with_comments(node)
343
- comment_list = comments_before(node)
344
-
345
- start_range = comment_list.any? ? comment_list.first.source_range : node.source_range
346
-
347
- start_range.join(node.source_range)
348
- end
349
-
350
- # Get priority for a category.
351
- #
352
- # @param [Symbol] category The category.
353
- # @param [RuboCop::AST::Node] _node The node.
354
- # @param [Boolean] is_model Whether this is a Rails model.
355
- # @return [Integer]
356
- def priority_for(category, _node, is_model)
357
- priorities = is_model ? MODEL_PRIORITIES : CLASS_PRIORITIES
358
-
359
- priorities[category] || 999
360
- end
361
-
362
- # Process body nodes to get a flat list.
363
- #
364
- # @param [RuboCop::AST::Node] body The body node.
365
- # @return [Array<RuboCop::AST::Node>]
366
- def process_body_nodes(body)
367
- if body.begin_type?
368
- body.children
369
- else
370
- [body]
160
+ CLASS_PRIORITIES
371
161
  end
372
162
  end
373
163
 
@@ -416,33 +206,6 @@ module RuboCop
416
206
  first_arg.value.to_s
417
207
  end
418
208
 
419
- # Categorize send nodes.
420
- #
421
- # @param [RuboCop::AST::Node] node The node to categorize.
422
- # @param [Symbol] _visibility The current visibility.
423
- # @param [Boolean] is_model Whether this is a Rails model.
424
- # @return [Symbol]
425
- # @return [nil]
426
- def send_category(node, _visibility, is_model)
427
- return :concerns if node.method?(:include)
428
- return nil unless is_model
429
-
430
- return :associations if ASSOCIATIONS.include?(node.method_name)
431
- return :validations if validation_method?(node)
432
- return :callbacks if CALLBACKS.include?(node.method_name)
433
- return :scopes if node.method?(:scope)
434
-
435
- nil
436
- end
437
-
438
- # Sort elements by priority, sort key, and original index.
439
- #
440
- # @param [Array<Hash>] elements The list of elements.
441
- # @return [Array<Hash>]
442
- def sort_elements(elements)
443
- elements.sort_by { |e| [e[:priority], e[:sort_key], e[:original_index]] }
444
- end
445
-
446
209
  # Get sort key for alphabetical ordering within category.
447
210
  #
448
211
  # @param [Symbol] category The category.
@@ -459,14 +222,6 @@ module RuboCop
459
222
  node.method_name.to_s
460
223
  end
461
224
 
462
- # Get the source of a node including preceding comments.
463
- #
464
- # @param [RuboCop::AST::Node] node The node.
465
- # @return [String]
466
- def source_with_comments(node)
467
- node_range_with_comments(node).source
468
- end
469
-
470
225
  # Check if node is a validation method.
471
226
  #
472
227
  # @param [RuboCop::AST::Node] node The node to check.
@@ -476,52 +231,15 @@ module RuboCop
476
231
  node.method_name.to_s.start_with?("validates_")
477
232
  end
478
233
 
479
- # Check if element violates alphabetical ordering.
234
+ # Get the violation message for the current context.
480
235
  #
481
- # @param [Hash] current The current element.
482
- # @param [Hash] following The following element.
483
- # @return [Boolean]
484
- def violates_alphabetical_order?(current, following)
485
- current[:priority] == following[:priority] &&
486
- !current[:sort_key].empty? &&
487
- current[:sort_key] > following[:sort_key]
488
- end
489
-
490
- # Check if element violates category ordering.
491
- #
492
- # @param [Hash] current The current element.
493
- # @param [Hash] following The following element.
494
- # @return [Boolean]
495
- def violates_category_order?(current, following)
496
- current[:priority] > following[:priority]
497
- end
498
-
499
- # Check if element violates ordering.
500
- #
501
- # @param [Hash] current The current element.
502
- # @param [Hash] following The following element.
503
- # @return [Boolean]
504
- def violates_order?(current, following)
505
- violates_category_order?(current, following) ||
506
- violates_alphabetical_order?(current, following)
507
- end
508
-
509
- # Get category for visibility-based instance methods.
510
- #
511
- # @param [Symbol] visibility The visibility.
512
- # @return [Symbol]
513
- # @return [nil]
514
- def visibility_method_category(visibility)
515
- VISIBILITY_CATEGORIES[visibility]
516
- end
517
-
518
- # Check if node is a visibility modifier (public, protected, private).
519
- #
520
- # @param [RuboCop::AST::Node] node The node to check.
521
- # @return [Boolean]
522
- def visibility_modifier?(node)
523
- node.send_type? && node.receiver.nil? &&
524
- %i(public protected private).include?(node.method_name)
236
+ # @return [String]
237
+ def violation_message
238
+ if @is_model
239
+ MODEL_MSG
240
+ else
241
+ CLASS_MSG
242
+ end
525
243
  end
526
244
  end
527
245
  end
@@ -94,7 +94,9 @@ module RuboCop
94
94
 
95
95
  return if statements.size < 2
96
96
 
97
- groups = group_consecutive_statements(statements, &:lvasgn_type?)
97
+ groups = group_consecutive_statements(statements) do |statement|
98
+ statement.lvasgn_type? && !statement.children.last.block_type?
99
+ end
98
100
 
99
101
  groups.each { |group| check_group_alignment(group) }
100
102
  end
@@ -92,7 +92,9 @@ module RuboCop
92
92
 
93
93
  return if statements.size < 2
94
94
 
95
- groups = group_consecutive_statements(statements) { |s| s.casgn_type? && s.single_line? }
95
+ groups = group_consecutive_statements(statements) do |statement|
96
+ statement.casgn_type? && statement.single_line?
97
+ end
96
98
 
97
99
  groups.each { |group| check_group_alignment(group) }
98
100
  end
@@ -112,7 +112,7 @@ module RuboCop
112
112
 
113
113
  return if statements.size < 2
114
114
 
115
- groups = group_consecutive_statements(statements) { |s| indexed_assignment?(s) }
115
+ groups = group_consecutive_statements(statements) { |statement| indexed_assignment?(statement) }
116
116
 
117
117
  groups.each { |group| check_group_alignment(group) }
118
118
  end
@@ -113,7 +113,7 @@ module RuboCop
113
113
 
114
114
  return if statements.size < 2
115
115
 
116
- groups = group_consecutive_statements(statements) { |s| let_declaration?(s) }
116
+ groups = group_consecutive_statements(statements) { |statement| let_declaration?(statement) }
117
117
 
118
118
  groups.each { |group| check_group_alignment(group) }
119
119
  end
@@ -109,7 +109,7 @@ module RuboCop
109
109
 
110
110
  return if statements.size < 2
111
111
 
112
- groups = group_consecutive_statements(statements) { |s| scope_declaration?(s) }
112
+ groups = group_consecutive_statements(statements) { |statement| scope_declaration?(statement) }
113
113
 
114
114
  groups.each { |group| check_group_alignment(group) }
115
115
  end
@@ -73,7 +73,7 @@ module RuboCop
73
73
  # @param [Array<RuboCop::AST::Node>] group The constants group.
74
74
  # @return [Boolean]
75
75
  def alphabetically_ordered?(group)
76
- names = group.map { |c| c.name.to_s }
76
+ names = group.map { |constant| constant.name.to_s }
77
77
 
78
78
  names == names.sort
79
79
  end
@@ -84,7 +84,7 @@ module RuboCop
84
84
  # @param [Array<RuboCop::AST::Node>] group The constants group.
85
85
  # @return [void]
86
86
  def autocorrect(corrector, group)
87
- sorted = group.sort_by { |c| c.name.to_s }
87
+ sorted = group.sort_by { |constant| constant.name.to_s }
88
88
 
89
89
  group.each_with_index do |constant, index|
90
90
  sorted_constant = sorted[index]
@@ -105,7 +105,7 @@ module RuboCop
105
105
  # @param [Array<Hash>] blocks The list of describe blocks.
106
106
  # @return [void]
107
107
  def autocorrect(corrector, blocks)
108
- sorted_blocks = blocks.sort_by { |b| [b[:priority], b[:original_index]] }
108
+ sorted_blocks = blocks.sort_by { |block| [block[:priority], block[:original_index]] }
109
109
 
110
110
  blocks.each_with_index do |block, index|
111
111
  sorted_block = sorted_blocks[index]
@@ -105,7 +105,7 @@ module RuboCop
105
105
 
106
106
  return if statements.size < 2
107
107
 
108
- groups = group_consecutive_statements(statements) { |s| let_declaration?(s) }
108
+ groups = group_consecutive_statements(statements) { |statement| let_declaration?(statement) }
109
109
 
110
110
  groups.each { |group| check_group_order(group) }
111
111
  end
@@ -10,6 +10,18 @@ module RuboCop
10
10
  module AlignmentHelpers
11
11
  private
12
12
 
13
+ # Check if two matched statements are consecutive (no gaps).
14
+ #
15
+ # @param [Array] first First [index, statement] pair.
16
+ # @param [Array] second Second [index, statement] pair.
17
+ # @param [Array<RuboCop::AST::Node>] statements Original statements.
18
+ # @return [Boolean]
19
+ def consecutive?(first, second, statements)
20
+ idx_a, stmt_a = first
21
+ idx_b, = second
22
+ idx_b == idx_a + 1 && no_blank_line_between?(stmt_a, statements[idx_b])
23
+ end
24
+
13
25
  # Extract statements from a body node.
14
26
  #
15
27
  # @param [RuboCop::AST::Node] body The body node.
@@ -22,20 +34,6 @@ module RuboCop
22
34
  end
23
35
  end
24
36
 
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
37
  # Find statements matching the predicate with their indices.
40
38
  #
41
39
  # @param [Array<RuboCop::AST::Node>] statements The statements.
@@ -53,20 +51,22 @@ module RuboCop
53
51
  # @return [Array<Array<RuboCop::AST::Node>>] Groups with 2+ consecutive statements.
54
52
  def group_by_consecutive_lines(matches, statements)
55
53
  matches
56
- .chunk_while { |a, b| consecutive?(a, b, statements) }
54
+ .chunk_while { |previous, current| consecutive?(previous, current, statements) }
57
55
  .filter_map { |group| group.map(&:last) if group.size > 1 }
58
56
  end
59
57
 
60
- # Check if two matched statements are consecutive (no gaps).
58
+ # Group consecutive statements based on a predicate.
61
59
  #
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])
60
+ # Iterates through statements and groups consecutive ones where the block
61
+ # returns true. Groups are broken by non-matching statements or blank lines.
62
+ #
63
+ # @param [Array<RuboCop::AST::Node>] statements The statements.
64
+ # @yield [RuboCop::AST::Node] Block to determine if statement matches criteria.
65
+ # @return [Array<Array<RuboCop::AST::Node>>] Groups of consecutive matching statements.
66
+ def group_consecutive_statements(statements, &)
67
+ matching_with_indices = find_matching_statements(statements, &)
68
+
69
+ group_by_consecutive_lines(matching_with_indices, statements)
70
70
  end
71
71
 
72
72
  # Check if there's no blank line between two statements.
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Vibe
6
+ # Shared helper methods for organizing class and module bodies.
7
+ #
8
+ # Including cops must implement:
9
+ # - categorize_send_node(node, visibility) -> Symbol or nil
10
+ # - priorities -> Hash
11
+ # - sort_key_for(category, node) -> String
12
+ # - violation_message -> String
13
+ module BodyOrganization
14
+ include OrganizationHelpers
15
+
16
+ private
17
+
18
+ # Get category for attr methods based on visibility.
19
+ def attr_category(visibility)
20
+ if visibility == :public
21
+ :attr_methods
22
+ else
23
+ visibility_method_category(visibility)
24
+ end
25
+ end
26
+
27
+ # Auto-correct by sorting elements within each visibility section.
28
+ def autocorrect(corrector, _container_node, elements)
29
+ elements.group_by { |element| element[:visibility] }.each_value do |section|
30
+ sorted = sort_elements(section)
31
+
32
+ next if sorted == section
33
+
34
+ replace_nodes(corrector, section, sorted)
35
+ end
36
+ end
37
+
38
+ # Categorize a node using shared logic and cop-specific send categorization.
39
+ def base_categorize_node(node, visibility)
40
+ return method_category(node, visibility) if node.any_def_type?
41
+ return :constants if node.casgn_type?
42
+ return nil if visibility_modifier?(node)
43
+ return attr_category(visibility) if attr_method?(node)
44
+
45
+ categorize_send_node(node, visibility)
46
+ end
47
+
48
+ # Build element hash for a node.
49
+ def build_body_element(child, visibility, index)
50
+ return unless child.type?(:send, :any_def, :casgn)
51
+
52
+ category = base_categorize_node(child, visibility)
53
+
54
+ return unless category
55
+
56
+ element_hash(child, category, visibility, index)
57
+ end
58
+
59
+ # Check and register violations.
60
+ def check_body_violations(container_node, elements)
61
+ violations = find_violations(elements)
62
+
63
+ return if violations.empty?
64
+
65
+ add_offense(violations.first[:node], message: violation_message) do |corrector|
66
+ autocorrect(corrector, container_node, elements)
67
+ end
68
+ end
69
+
70
+ # Collect elements from body nodes.
71
+ def collect_body_elements(body)
72
+ visibility = :public
73
+ elements = []
74
+
75
+ process_body_nodes(body).each do |child|
76
+ visibility = child.method_name if visibility_modifier?(child)
77
+ element = build_body_element(child, visibility, elements.size)
78
+
79
+ elements << element if element
80
+ end
81
+
82
+ elements
83
+ end
84
+
85
+ # Create element hash.
86
+ def element_hash(node, category, visibility, index)
87
+ {
88
+ node:, category:, visibility:,
89
+ original_index: index,
90
+ priority: priorities[category] || 999,
91
+ sort_key: sort_key_for(category, node)
92
+ }
93
+ end
94
+
95
+ # Find elements that violate ordering.
96
+ def find_violations(elements)
97
+ elements.each_cons(2).filter_map do |current, following|
98
+ following if violates_order?(current, following)
99
+ end.uniq
100
+ end
101
+
102
+ # Categorize method nodes.
103
+ def method_category(node, visibility)
104
+ return :class_methods if node.defs_type?
105
+ return :initialize if node.method?(:initialize) && visibility == :public
106
+
107
+ visibility_method_category(visibility)
108
+ end
109
+
110
+ # Sort elements by priority, sort key, and original index.
111
+ def sort_elements(elements)
112
+ elements.sort_by { |element| [element[:priority], element[:sort_key], element[:original_index]] }
113
+ end
114
+
115
+ # Check if element violates alphabetical ordering.
116
+ def violates_alphabetical_order?(current, following)
117
+ current[:priority] == following[:priority] &&
118
+ !current[:sort_key].empty? &&
119
+ current[:sort_key] > following[:sort_key]
120
+ end
121
+
122
+ # Check if element violates ordering.
123
+ def violates_order?(current, following)
124
+ current[:priority] > following[:priority] ||
125
+ violates_alphabetical_order?(current, following)
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Vibe
6
+ # Low-level helpers for node ordering and comment handling.
7
+ #
8
+ # Used by BodyOrganization to handle source ranges, comments,
9
+ # visibility modifiers, and node replacement during autocorrect.
10
+ module OrganizationHelpers
11
+ ATTR_SORT_ORDER = { attr_accessor: "!0", attr_reader: "!1", attr_writer: "!2" }.freeze
12
+ VISIBILITY_CATEGORIES = {
13
+ protected: :protected_methods,
14
+ private: :private_methods,
15
+ public: :instance_methods
16
+ }.freeze
17
+
18
+ private
19
+
20
+ # Check if node is an attr method.
21
+ def attr_method?(node)
22
+ node.send_type? && ATTR_SORT_ORDER.key?(node.method_name)
23
+ end
24
+
25
+ # Get comments immediately before a node.
26
+ def comments_before(node)
27
+ consecutive = []
28
+ expected_line = node.first_line - 1
29
+
30
+ while (comment = comments_by_line[expected_line])
31
+ consecutive.unshift(comment)
32
+ expected_line -= 1
33
+ end
34
+
35
+ consecutive
36
+ end
37
+
38
+ # Get comments indexed by line number for fast lookup.
39
+ def comments_by_line
40
+ @comments_by_line ||= processed_source.comments.to_h { |comment| [comment.location.line, comment] }
41
+ end
42
+
43
+ # Get the source range of a node including preceding comments.
44
+ def node_range_with_comments(node)
45
+ comment_list = comments_before(node)
46
+ start_range = comment_list.any? ? comment_list.first.source_range : node.source_range
47
+ start_range.join(node.source_range)
48
+ end
49
+
50
+ # Process body nodes to get a flat list.
51
+ def process_body_nodes(body)
52
+ if body.begin_type?
53
+ body.children
54
+ else
55
+ [body]
56
+ end
57
+ end
58
+
59
+ # Replace nodes during auto-correct.
60
+ def replace_nodes(corrector, group, sorted)
61
+ sorted.each_with_index do |element, idx|
62
+ original = group[idx]
63
+
64
+ next if element[:node] == original[:node]
65
+
66
+ corrector.replace(node_range_with_comments(original[:node]),
67
+ node_range_with_comments(element[:node]).source)
68
+ end
69
+ end
70
+
71
+ # Get category for visibility-based instance methods.
72
+ def visibility_method_category(visibility)
73
+ VISIBILITY_CATEGORIES[visibility]
74
+ end
75
+
76
+ # Check if node is a visibility modifier.
77
+ def visibility_modifier?(node)
78
+ node.send_type? && node.receiver.nil? &&
79
+ %i(public protected private).include?(node.method_name)
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Vibe
6
+ # Enforces consistent organization of module definitions.
7
+ #
8
+ # Enforces: includes/extends → constants → attr → class methods →
9
+ # instance methods → protected → private.
10
+ #
11
+ # Within each category, methods are sorted alphabetically.
12
+ #
13
+ # @example
14
+ # # bad
15
+ # module Helper
16
+ # def zebra
17
+ # end
18
+ #
19
+ # def alpha
20
+ # end
21
+ #
22
+ # include Foo
23
+ # end
24
+ #
25
+ # # good
26
+ # module Helper
27
+ # include Foo
28
+ #
29
+ # def alpha
30
+ # end
31
+ #
32
+ # def zebra
33
+ # end
34
+ # end
35
+ class ModuleOrganization < Base
36
+ extend AutoCorrector
37
+ include BodyOrganization
38
+
39
+ MODULE_MSG = "Module elements should be ordered: includes/extends → constants → attr → " \
40
+ "class methods → instance methods → protected → private."
41
+ MODULE_PRIORITIES = {
42
+ concerns: 10,
43
+ constants: 20,
44
+ attr_methods: 25,
45
+ class_methods: 35,
46
+ instance_methods: 40,
47
+ protected_methods: 50,
48
+ private_methods: 60
49
+ }.freeze
50
+
51
+ # Check module nodes for organization.
52
+ #
53
+ # @param [RuboCop::AST::Node] node The module node.
54
+ # @return [void]
55
+ def on_module(node)
56
+ return unless node.body
57
+
58
+ elements = collect_body_elements(node.body)
59
+
60
+ return if elements.size < 2
61
+
62
+ check_body_violations(node, elements)
63
+ end
64
+
65
+ private
66
+
67
+ # Categorize send nodes for modules.
68
+ #
69
+ # @param [RuboCop::AST::Node] node The node to categorize.
70
+ # @param [Symbol] _visibility The current visibility.
71
+ # @return [Symbol]
72
+ # @return [nil]
73
+ def categorize_send_node(node, _visibility)
74
+ return :concerns if node.method?(:include) || node.method?(:extend)
75
+
76
+ nil
77
+ end
78
+
79
+ # Get the priority hash.
80
+ #
81
+ # @return [Hash]
82
+ def priorities
83
+ MODULE_PRIORITIES
84
+ end
85
+
86
+ # Get sort key for alphabetical ordering within category.
87
+ #
88
+ # @param [Symbol] category The category.
89
+ # @param [RuboCop::AST::Node] node The node.
90
+ # @return [String]
91
+ def sort_key_for(category, node)
92
+ sortable = %i(class_methods instance_methods protected_methods private_methods)
93
+
94
+ return "" unless sortable.include?(category)
95
+ return ATTR_SORT_ORDER[node.method_name] if attr_method?(node)
96
+
97
+ node.method_name.to_s
98
+ end
99
+
100
+ # Get the violation message.
101
+ #
102
+ # @return [String]
103
+ def violation_message
104
+ MODULE_MSG
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -97,7 +97,7 @@ module RuboCop
97
97
  # @param [RuboCop::AST::Node] node The node.
98
98
  # @return [RuboCop::AST::Node, nil]
99
99
  def conditional_ancestor(node)
100
- node.each_ancestor.find { |a| a.type?(:if, :while, :until, :when) }
100
+ node.each_ancestor.find { |ancestor| ancestor.type?(:if, :while, :until, :when) }
101
101
  end
102
102
 
103
103
  # Check if node is in a conditional position (if/unless/while/until/when condition).
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Vibe
6
+ # Disallows single character variable names.
7
+ #
8
+ # Single character names tend to obscure intent. The cop flags local
9
+ # variables, instance variables, class variables, method and block
10
+ # parameters, and rescue variables. The leading `@` or `@@` sigil is
11
+ # ignored when measuring length, so `@a` and `@@a` are treated as
12
+ # single character names.
13
+ #
14
+ # Names listed in `AllowedNames` are ignored. The default list contains
15
+ # `_`, which conventionally marks an intentionally unused value.
16
+ #
17
+ # @example
18
+ # # bad
19
+ # x = 1
20
+ # @a = 1
21
+ # def foo(n); end
22
+ # [1].each { |i| i }
23
+ #
24
+ # # good
25
+ # count = 1
26
+ # @account = 1
27
+ # def foo(name); end
28
+ # [1].each { |index| index }
29
+ #
30
+ # @example AllowedNames: ['_', 'i', 'j']
31
+ # # good
32
+ # 2.times { |i| i }
33
+ #
34
+ # The block parameter `t` is allowed in files under `db/`, where it is a
35
+ # convention for Rails migrations (e.g. `create_table :users do |t|`).
36
+ class NoSingleCharacterVariableNames < Base
37
+ MIGRATION_BLOCK_VARIABLE = "t"
38
+ MIGRATION_PATH_PATTERN = %r{(?:\A|/)db/}
39
+ MSG = "Avoid single character variable name `%<name>s`."
40
+
41
+ # @param node [RuboCop::AST::Node]
42
+ # @return [void]
43
+ def on_arg(node)
44
+ check(node, node.name)
45
+ end
46
+ alias on_blockarg on_arg
47
+ alias on_cvasgn on_arg
48
+ alias on_ivasgn on_arg
49
+ alias on_kwarg on_arg
50
+ alias on_kwoptarg on_arg
51
+ alias on_kwrestarg on_arg
52
+ alias on_lvasgn on_arg
53
+ alias on_optarg on_arg
54
+ alias on_restarg on_arg
55
+
56
+ private
57
+
58
+ # @return [Array<String>]
59
+ def allowed_names
60
+ @allowed_names ||= Array(cop_config.fetch("AllowedNames", ["_"]))
61
+ end
62
+
63
+ # @param node [RuboCop::AST::Node]
64
+ # @param name [Symbol, nil]
65
+ # @return [void]
66
+ def check(node, name)
67
+ return if name.nil?
68
+
69
+ bare = name.to_s.sub(/\A@@?/, "")
70
+
71
+ return if bare.length != 1
72
+ return if allowed_names.include?(bare)
73
+ return if migration_block_variable?(bare)
74
+
75
+ add_offense(node.loc.name, message: format(MSG, name: name))
76
+ end
77
+
78
+ # @param bare [String] the variable name without sigils.
79
+ # @return [Boolean]
80
+ def migration_block_variable?(bare)
81
+ bare == MIGRATION_BLOCK_VARIABLE && migration_file?
82
+ end
83
+
84
+ # @return [Boolean]
85
+ def migration_file?
86
+ processed_source.file_path.match?(MIGRATION_PATH_PATTERN)
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -109,7 +109,7 @@ module RuboCop
109
109
  # @param [Array<RuboCop::AST::Node>] chain The method chain.
110
110
  # @return [Boolean]
111
111
  def chain_has_with?(chain)
112
- chain.any? { |n| n.method?(:with) }
112
+ chain.any? { |node| node.method?(:with) }
113
113
  end
114
114
 
115
115
  # Check if the method is one we care about for chaining.
@@ -190,7 +190,7 @@ module RuboCop
190
190
  # @param [Array<RuboCop::AST::Node>] chain The method chain.
191
191
  # @return [Integer, nil]
192
192
  def find_receive_index(chain)
193
- chain.index { |n| RECEIVE_METHODS.include?(n.method_name) }
193
+ chain.index { |node| RECEIVE_METHODS.include?(node.method_name) }
194
194
  end
195
195
 
196
196
  # Find the starting position for the replacement.
@@ -66,8 +66,8 @@ module RuboCop
66
66
  # @param [Array<RuboCop::AST::Node>] group The validates group.
67
67
  # @return [void]
68
68
  def autocorrect(corrector, group)
69
- validates_nodes = group.select { |n| VALIDATES_METHODS.include?(n.method_name) }
70
- validate_nodes = group.select { |n| n.method?(VALIDATE_METHOD) }
69
+ validates_nodes = group.select { |node| VALIDATES_METHODS.include?(node.method_name) }
70
+ validate_nodes = group.select { |node| node.method?(VALIDATE_METHOD) }
71
71
  sorted = validates_nodes + validate_nodes
72
72
 
73
73
  group.each_with_index do |node, index|
@@ -104,7 +104,7 @@ module RuboCop
104
104
 
105
105
  return if statements.size < 2
106
106
 
107
- groups = group_consecutive_statements(statements) { |s| validates_family?(s) }
107
+ groups = group_consecutive_statements(statements) { |statement| validates_family?(statement) }
108
108
 
109
109
  groups.each { |group| check_group_order(group) }
110
110
  end
@@ -114,7 +114,7 @@ module RuboCop
114
114
  # @param [Array<RuboCop::AST::Node>] group The validates group.
115
115
  # @return [Array<RuboCop::AST::Node>] Validate nodes that violate ordering.
116
116
  def find_violations(group)
117
- last_validates_index = group.rindex { |n| VALIDATES_METHODS.include?(n.method_name) }
117
+ last_validates_index = group.rindex { |node| VALIDATES_METHODS.include?(node.method_name) }
118
118
 
119
119
  return [] if last_validates_index.nil?
120
120
 
@@ -60,7 +60,7 @@ module RuboCop
60
60
  # @param [Array<RuboCop::AST::Node>] group The validates group.
61
61
  # @return [Boolean]
62
62
  def alphabetically_ordered?(group)
63
- names = group.map { |v| extract_validates_name(v) }
63
+ names = group.map { |validates| extract_validates_name(validates) }
64
64
 
65
65
  names == names.sort
66
66
  end
@@ -71,7 +71,7 @@ module RuboCop
71
71
  # @param [Array<RuboCop::AST::Node>] group The validates group.
72
72
  # @return [void]
73
73
  def autocorrect(corrector, group)
74
- sorted = group.sort_by { |v| extract_validates_name(v) }
74
+ sorted = group.sort_by { |validates| extract_validates_name(validates) }
75
75
 
76
76
  group.each_with_index do |validates, index|
77
77
  sorted_validates = sorted[index]
@@ -107,7 +107,7 @@ module RuboCop
107
107
 
108
108
  return if statements.size < 2
109
109
 
110
- groups = group_consecutive_statements(statements) { |s| validates_declaration?(s) }
110
+ groups = group_consecutive_statements(statements) { |statement| validates_declaration?(statement) }
111
111
 
112
112
  groups.each { |group| check_group_order(group) }
113
113
  end
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "vibe/mixin/alignment_helpers"
4
+ require_relative "vibe/mixin/organization_helpers"
5
+ require_relative "vibe/mixin/body_organization"
4
6
  require_relative "vibe/mixin/spec_file_helper"
5
7
 
6
8
  require_relative "vibe/attr_order"
@@ -19,10 +21,12 @@ require_relative "vibe/explicit_return_conditional"
19
21
  require_relative "vibe/is_expected_one_liner"
20
22
  require_relative "vibe/keyword_argument_order"
21
23
  require_relative "vibe/let_order"
24
+ require_relative "vibe/module_organization"
22
25
  require_relative "vibe/multiline_hash_argument_style"
23
26
  require_relative "vibe/no_assigns_attribute_testing"
24
27
  require_relative "vibe/no_compound_conditions"
25
28
  require_relative "vibe/no_rubocop_disable"
29
+ require_relative "vibe/no_single_character_variable_names"
26
30
  require_relative "vibe/no_skipped_tests"
27
31
  require_relative "vibe/no_unless_guard_clause"
28
32
  require_relative "vibe/prefer_one_liner_expectation"
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RuboCop
4
4
  module Vibe
5
- VERSION = "0.6.0"
5
+ VERSION = "0.7.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.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tristan Dunn
@@ -104,11 +104,15 @@ files:
104
104
  - lib/rubocop/cop/vibe/keyword_argument_order.rb
105
105
  - lib/rubocop/cop/vibe/let_order.rb
106
106
  - lib/rubocop/cop/vibe/mixin/alignment_helpers.rb
107
+ - lib/rubocop/cop/vibe/mixin/body_organization.rb
108
+ - lib/rubocop/cop/vibe/mixin/organization_helpers.rb
107
109
  - lib/rubocop/cop/vibe/mixin/spec_file_helper.rb
110
+ - lib/rubocop/cop/vibe/module_organization.rb
108
111
  - lib/rubocop/cop/vibe/multiline_hash_argument_style.rb
109
112
  - lib/rubocop/cop/vibe/no_assigns_attribute_testing.rb
110
113
  - lib/rubocop/cop/vibe/no_compound_conditions.rb
111
114
  - lib/rubocop/cop/vibe/no_rubocop_disable.rb
115
+ - lib/rubocop/cop/vibe/no_single_character_variable_names.rb
112
116
  - lib/rubocop/cop/vibe/no_skipped_tests.rb
113
117
  - lib/rubocop/cop/vibe/no_unless_guard_clause.rb
114
118
  - lib/rubocop/cop/vibe/prefer_one_liner_expectation.rb
@@ -144,7 +148,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
144
148
  - !ruby/object:Gem::Version
145
149
  version: '0'
146
150
  requirements: []
147
- rubygems_version: 4.0.9
151
+ rubygems_version: 4.0.23
148
152
  specification_version: 4
149
153
  summary: A set of custom cops to use on AI generated code.
150
154
  test_files: []